package authproxy

import (
	"encoding/base32"
	"fmt"
	"net"
	"net/mail"
	"reflect"
	"strings"
	"time"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/remotecache"
	"github.com/grafana/grafana/pkg/models"
	"github.com/grafana/grafana/pkg/services/ldap"
	"github.com/grafana/grafana/pkg/services/multildap"
	"github.com/grafana/grafana/pkg/setting"
	"github.com/grafana/grafana/pkg/util"
)

const (

	// CachePrefix is a prefix for the cache key
	CachePrefix = "auth-proxy-sync-ttl:%s"
)

// getLDAPConfig gets LDAP config
var getLDAPConfig = ldap.GetConfig

// isLDAPEnabled checks if LDAP is enabled
var isLDAPEnabled = ldap.IsEnabled

// newLDAP creates multiple LDAP instance
var newLDAP = multildap.New

// supportedHeaders states the supported headers configuration fields
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}

// AuthProxy struct
type AuthProxy struct {
	store  *remotecache.RemoteCache
	ctx    *models.ReqContext
	orgID  int64
	header string

	enabled             bool
	LDAPAllowSignup     bool
	AuthProxyAutoSignUp bool
	whitelistIP         string
	headerType          string
	headers             map[string]string
	cacheTTL            int
}

// Error auth proxy specific error
type Error struct {
	Message      string
	DetailsError error
}

// newError creates the Error
func newError(message string, err error) *Error {
	return &Error{
		Message:      message,
		DetailsError: err,
	}
}

// Error returns a Error error string
func (err *Error) Error() string {
	return err.Message
}

// Options for the AuthProxy
type Options struct {
	Store *remotecache.RemoteCache
	Ctx   *models.ReqContext
	OrgID int64
}

// New instance of the AuthProxy
func New(options *Options) *AuthProxy {
	header := options.Ctx.Req.Header.Get(setting.AuthProxyHeaderName)

	return &AuthProxy{
		store:  options.Store,
		ctx:    options.Ctx,
		orgID:  options.OrgID,
		header: header,

		enabled:             setting.AuthProxyEnabled,
		headerType:          setting.AuthProxyHeaderProperty,
		headers:             setting.AuthProxyHeaders,
		whitelistIP:         setting.AuthProxyWhitelist,
		cacheTTL:            setting.AuthProxyLDAPSyncTtl,
		LDAPAllowSignup:     setting.LDAPAllowSignup,
		AuthProxyAutoSignUp: setting.AuthProxyAutoSignUp,
	}
}

// IsEnabled checks if the proxy auth is enabled
func (auth *AuthProxy) IsEnabled() bool {

	// Bail if the setting is not enabled
	return auth.enabled
}

// HasHeader checks if the we have specified header
func (auth *AuthProxy) HasHeader() bool {
	return len(auth.header) != 0
}

// IsAllowedIP compares presented IP with the whitelist one
func (auth *AuthProxy) IsAllowedIP() (bool, *Error) {
	ip := auth.ctx.Req.RemoteAddr

	if len(strings.TrimSpace(auth.whitelistIP)) == 0 {
		return true, nil
	}

	proxies := strings.Split(auth.whitelistIP, ",")
	var proxyObjs []*net.IPNet
	for _, proxy := range proxies {
		result, err := coerceProxyAddress(proxy)
		if err != nil {
			return false, newError("Could not get the network", err)
		}

		proxyObjs = append(proxyObjs, result)
	}

	sourceIP, _, _ := net.SplitHostPort(ip)
	sourceObj := net.ParseIP(sourceIP)

	for _, proxyObj := range proxyObjs {
		if proxyObj.Contains(sourceObj) {
			return true, nil
		}
	}

	err := fmt.Errorf(
		"Request for user (%s) from %s is not from the authentication proxy", auth.header,
		sourceIP,
	)

	return false, newError("Proxy authentication required", err)
}

// getKey forms a key for the cache based on the headers received as part of the authentication flow.
// Our configuration supports multiple headers. The main header contains the email or username.
// And the additional ones that allow us to specify extra attributes: Name, Email or Groups.
func (auth *AuthProxy) getKey() string {
	key := strings.TrimSpace(auth.header) // start the key with the main header

	auth.headersIterator(func(_, header string) {
		key = strings.Join([]string{key, header}, "-") // compose the key with any additional headers
	})

	hashedKey := base32.StdEncoding.EncodeToString([]byte(key))
	return fmt.Sprintf(CachePrefix, hashedKey)
}

// Login logs in user id with whatever means possible
func (auth *AuthProxy) Login() (int64, *Error) {

	id, _ := auth.GetUserViaCache()
	if id != 0 {
		// Error here means absent cache - we don't need to handle that
		return id, nil
	}

	if isLDAPEnabled() {
		id, err := auth.LoginViaLDAP()

		if err == ldap.ErrInvalidCredentials {
			return 0, newError(
				"Proxy authentication required",
				ldap.ErrInvalidCredentials,
			)
		}

		if err != nil {
			return 0, newError("Failed to get the user", err)
		}

		return id, nil
	}

	id, err := auth.LoginViaHeader()
	if err != nil {
		return 0, newError(
			"Failed to log in as user, specified in auth proxy header",
			err,
		)
	}

	return id, nil
}

// GetUserViaCache gets user id from cache
func (auth *AuthProxy) GetUserViaCache() (int64, error) {
	var (
		cacheKey    = auth.getKey()
		userID, err = auth.store.Get(cacheKey)
	)

	if err != nil {
		return 0, err
	}

	return userID.(int64), nil
}

// LoginViaLDAP logs in user via LDAP request
func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
	config, err := getLDAPConfig()
	if err != nil {
		return 0, newError("Failed to get LDAP config", nil)
	}

	extUser, err := newLDAP(config.Servers).User(auth.header)
	if err != nil {
		return 0, newError(err.Error(), nil)
	}

	// Have to sync grafana and LDAP user during log in
	upsert := &models.UpsertUserCommand{
		ReqContext:    auth.ctx,
		SignupAllowed: auth.LDAPAllowSignup,
		ExternalUser:  extUser,
	}
	err = bus.Dispatch(upsert)
	if err != nil {
		return 0, newError(err.Error(), nil)
	}

	return upsert.Result.Id, nil
}

// LoginViaHeader logs in user from the header only
// TODO: refactor - cyclomatic complexity should be much lower
func (auth *AuthProxy) LoginViaHeader() (int64, error) {
	extUser := &models.ExternalUserInfo{
		AuthModule: "authproxy",
		AuthId:     auth.header,
	}

	switch auth.headerType {
	case "username":
		extUser.Login = auth.header

		emailAddr, emailErr := mail.ParseAddress(auth.header) // only set Email if it can be parsed as an email address
		if emailErr == nil {
			extUser.Email = emailAddr.Address
		}
	case "email":
		extUser.Email = auth.header
		extUser.Login = auth.header
	default:
		return 0, newError("Auth proxy header property invalid", nil)

	}

	auth.headersIterator(func(field string, header string) {
		if field == "Groups" {
			extUser.Groups = util.SplitString(header)
		} else {
			reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
		}
	})

	upsert := &models.UpsertUserCommand{
		ReqContext:    auth.ctx,
		SignupAllowed: setting.AuthProxyAutoSignUp,
		ExternalUser:  extUser,
	}

	err := bus.Dispatch(upsert)
	if err != nil {
		return 0, err
	}

	return upsert.Result.Id, nil
}

// headersIterator iterates over all non-empty supported additional headers
func (auth *AuthProxy) headersIterator(fn func(field string, header string)) {
	for _, field := range supportedHeaderFields {
		h := auth.headers[field]

		if h == "" {
			continue
		}

		if value := auth.ctx.Req.Header.Get(h); value != "" {
			fn(field, strings.TrimSpace(value))
		}
	}
}

// GetSignedUser get full signed user info
func (auth *AuthProxy) GetSignedUser(userID int64) (*models.SignedInUser, *Error) {
	query := &models.GetSignedInUserQuery{
		OrgId:  auth.orgID,
		UserId: userID,
	}

	if err := bus.Dispatch(query); err != nil {
		return nil, newError(err.Error(), nil)
	}

	return query.Result, nil
}

// Remember user in cache
func (auth *AuthProxy) Remember(id int64) *Error {
	key := auth.getKey()

	// Check if user already in cache
	userID, _ := auth.store.Get(key)
	if userID != nil {
		return nil
	}

	expiration := time.Duration(auth.cacheTTL) * time.Minute

	err := auth.store.Set(key, id, expiration)
	if err != nil {
		return newError(err.Error(), nil)
	}

	return nil
}

// coerceProxyAddress gets network of the presented CIDR notation
func coerceProxyAddress(proxyAddr string) (*net.IPNet, error) {
	proxyAddr = strings.TrimSpace(proxyAddr)
	if !strings.Contains(proxyAddr, "/") {
		proxyAddr = strings.Join([]string{proxyAddr, "32"}, "/")
	}

	_, network, err := net.ParseCIDR(proxyAddr)
	return network, err
}
