package authproxy

import (
	"encoding/hex"
	"errors"
	"fmt"
	"hash/fnv"
	"net"
	"net/mail"
	"path"
	"reflect"
	"strings"
	"time"

	"github.com/grafana/grafana/pkg/bus"
	"github.com/grafana/grafana/pkg/infra/log"
	"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 = func(cfg *setting.Cfg) bool {
	if cfg != nil {
		return cfg.LDAPEnabled
	}

	return setting.LDAPEnabled
}

// 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 {
	cfg         *setting.Cfg
	remoteCache *remotecache.RemoteCache
	ctx         *models.ReqContext
	orgID       int64
	header      string
}

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

// newError returns an Error.
func newError(message string, err error) Error {
	return Error{
		Message:      message,
		DetailsError: err,
	}
}

// Error returns the error message.
func (err Error) Error() string {
	return err.Message
}

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

// New instance of the AuthProxy.
func New(cfg *setting.Cfg, options *Options) *AuthProxy {
	header := options.Ctx.Req.Header.Get(cfg.AuthProxyHeaderName)
	return &AuthProxy{
		remoteCache: options.RemoteCache,
		cfg:         cfg,
		ctx:         options.Ctx,
		orgID:       options.OrgID,
		header:      header,
	}
}

// IsEnabled checks if the auth proxy is enabled.
func (auth *AuthProxy) IsEnabled() bool {
	// Bail if the setting is not enabled
	return auth.cfg.AuthProxyEnabled
}

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

// IsAllowedIP returns whether provided IP is allowed.
func (auth *AuthProxy) IsAllowedIP() error {
	ip := auth.ctx.Req.RemoteAddr

	if len(strings.TrimSpace(auth.cfg.AuthProxyWhitelist)) == 0 {
		return nil
	}

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

		proxyObjs = append(proxyObjs, result)
	}

	sourceIP, _, err := net.SplitHostPort(ip)
	if err != nil {
		return newError("could not parse address", err)
	}
	sourceObj := net.ParseIP(sourceIP)

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

	return newError("proxy authentication required", fmt.Errorf(
		"request for user (%s) from %s is not from the authentication proxy", auth.header,
		sourceIP,
	))
}

func HashCacheKey(key string) (string, error) {
	hasher := fnv.New128a()
	if _, err := hasher.Write([]byte(key)); err != nil {
		return "", err
	}
	return hex.EncodeToString(hasher.Sum(nil)), nil
}

// 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, error) {
	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, err := HashCacheKey(key)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf(CachePrefix, hashedKey), nil
}

// Login logs in user ID by whatever means possible.
func (auth *AuthProxy) Login(logger log.Logger, ignoreCache bool) (int64, error) {
	if !ignoreCache {
		// Error here means absent cache - we don't need to handle that
		id, err := auth.GetUserViaCache(logger)
		if err == nil && id != 0 {
			return id, nil
		}
	}

	if isLDAPEnabled(auth.cfg) {
		id, err := auth.LoginViaLDAP()
		if err != nil {
			if errors.Is(err, ldap.ErrInvalidCredentials) {
				return 0, newError("proxy authentication required", ldap.ErrInvalidCredentials)
			}
			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(logger log.Logger) (int64, error) {
	cacheKey, err := auth.getKey()
	if err != nil {
		return 0, err
	}
	logger.Debug("Getting user ID via auth cache", "cacheKey", cacheKey)
	userID, err := auth.remoteCache.Get(cacheKey)
	if err != nil {
		logger.Debug("Failed getting user ID via auth cache", "error", err)
		return 0, err
	}

	logger.Debug("Successfully got user ID via auth cache", "id", userID)
	return userID.(int64), nil
}

// RemoveUserFromCache removes user from cache.
func (auth *AuthProxy) RemoveUserFromCache(logger log.Logger) error {
	cacheKey, err := auth.getKey()
	if err != nil {
		return err
	}
	logger.Debug("Removing user from auth cache", "cacheKey", cacheKey)
	if err := auth.remoteCache.Delete(cacheKey); err != nil {
		return err
	}

	logger.Debug("Successfully removed user from auth cache", "cacheKey", cacheKey)
	return nil
}

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

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

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

	return upsert.Result.Id, nil
}

// LoginViaHeader logs in user from the header only
func (auth *AuthProxy) LoginViaHeader() (int64, error) {
	extUser := &models.ExternalUserInfo{
		AuthModule: "authproxy",
		AuthId:     auth.header,
	}

	switch auth.cfg.AuthProxyHeaderProperty {
	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, fmt.Errorf("auth proxy header property invalid")
	}

	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: auth.cfg.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.cfg.AuthProxyHeaders[field]
		if h == "" {
			continue
		}

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

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

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

	return query.Result, nil
}

// Remember user in cache
func (auth *AuthProxy) Remember(id int64) error {
	key, err := auth.getKey()
	if err != nil {
		return err
	}

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

	expiration := time.Duration(auth.cfg.AuthProxySyncTTL) * time.Minute

	if err := auth.remoteCache.Set(key, id, expiration); err != nil {
		return err
	}

	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 = path.Join(proxyAddr, "32")
	}

	_, network, err := net.ParseCIDR(proxyAddr)
	if err != nil {
		return nil, fmt.Errorf("could not parse the network: %w", err)
	}
	return network, nil
}
