package dataprovider

import (
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"os"
	"regexp"
	"strings"

	"github.com/alexedwards/argon2id"
	passwordvalidator "github.com/wagslane/go-password-validator"
	"golang.org/x/crypto/bcrypt"

	"github.com/drakkan/sftpgo/v2/kms"
	"github.com/drakkan/sftpgo/v2/logger"
	"github.com/drakkan/sftpgo/v2/mfa"
	"github.com/drakkan/sftpgo/v2/sdk"
	"github.com/drakkan/sftpgo/v2/util"
)

// Available permissions for SFTPGo admins
const (
	PermAdminAny              = "*"
	PermAdminAddUsers         = "add_users"
	PermAdminChangeUsers      = "edit_users"
	PermAdminDeleteUsers      = "del_users"
	PermAdminViewUsers        = "view_users"
	PermAdminViewConnections  = "view_conns"
	PermAdminCloseConnections = "close_conns"
	PermAdminViewServerStatus = "view_status"
	PermAdminManageAdmins     = "manage_admins"
	PermAdminManageAPIKeys    = "manage_apikeys"
	PermAdminQuotaScans       = "quota_scans"
	PermAdminManageSystem     = "manage_system"
	PermAdminManageDefender   = "manage_defender"
	PermAdminViewDefender     = "view_defender"
	PermAdminRetentionChecks  = "retention_checks"
	PermAdminViewEvents       = "view_events"
)

var (
	emailRegex      = regexp.MustCompile("^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$")
	validAdminPerms = []string{PermAdminAny, PermAdminAddUsers, PermAdminChangeUsers, PermAdminDeleteUsers,
		PermAdminViewUsers, PermAdminViewConnections, PermAdminCloseConnections, PermAdminViewServerStatus,
		PermAdminManageAdmins, PermAdminManageAPIKeys, PermAdminQuotaScans, PermAdminManageSystem,
		PermAdminManageDefender, PermAdminViewDefender, PermAdminRetentionChecks, PermAdminViewEvents}
)

// TOTPConfig defines the time-based one time password configuration
type TOTPConfig struct {
	Enabled    bool        `json:"enabled,omitempty"`
	ConfigName string      `json:"config_name,omitempty"`
	Secret     *kms.Secret `json:"secret,omitempty"`
}

func (c *TOTPConfig) validate(username string) error {
	if !c.Enabled {
		c.ConfigName = ""
		c.Secret = kms.NewEmptySecret()
		return nil
	}
	if c.ConfigName == "" {
		return util.NewValidationError("totp: config name is mandatory")
	}
	if !util.IsStringInSlice(c.ConfigName, mfa.GetAvailableTOTPConfigNames()) {
		return util.NewValidationError(fmt.Sprintf("totp: config name %#v not found", c.ConfigName))
	}
	if c.Secret.IsEmpty() {
		return util.NewValidationError("totp: secret is mandatory")
	}
	if c.Secret.IsPlain() {
		c.Secret.SetAdditionalData(username)
		if err := c.Secret.Encrypt(); err != nil {
			return util.NewValidationError(fmt.Sprintf("totp: unable to encrypt secret: %v", err))
		}
	}
	return nil
}

// AdminFilters defines additional restrictions for SFTPGo admins
// TODO: rename to AdminOptions in v3
type AdminFilters struct {
	// only clients connecting from these IP/Mask are allowed.
	// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
	// for example "192.0.2.0/24" or "2001:db8::/32"
	AllowList []string `json:"allow_list,omitempty"`
	// API key auth allows to impersonate this administrator with an API key
	AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
	// Time-based one time passwords configuration
	TOTPConfig TOTPConfig `json:"totp_config,omitempty"`
	// Recovery codes to use if the user loses access to their second factor auth device.
	// Each code can only be used once, you should use these codes to login and disable or
	// reset 2FA for your account
	RecoveryCodes []sdk.RecoveryCode `json:"recovery_codes,omitempty"`
}

// Admin defines a SFTPGo admin
type Admin struct {
	// Database unique identifier
	ID int64 `json:"id"`
	// 1 enabled, 0 disabled (login is not allowed)
	Status int `json:"status"`
	// Username
	Username       string       `json:"username"`
	Password       string       `json:"password,omitempty"`
	Email          string       `json:"email,omitempty"`
	Permissions    []string     `json:"permissions"`
	Filters        AdminFilters `json:"filters,omitempty"`
	Description    string       `json:"description,omitempty"`
	AdditionalInfo string       `json:"additional_info,omitempty"`
	// Creation time as unix timestamp in milliseconds. It will be 0 for admins created before v2.2.0
	CreatedAt int64 `json:"created_at"`
	// last update time as unix timestamp in milliseconds
	UpdatedAt int64 `json:"updated_at"`
	// Last login as unix timestamp in milliseconds
	LastLogin int64 `json:"last_login"`
}

// CountUnusedRecoveryCodes returns the number of unused recovery codes
func (a *Admin) CountUnusedRecoveryCodes() int {
	unused := 0
	for _, code := range a.Filters.RecoveryCodes {
		if !code.Used {
			unused++
		}
	}
	return unused
}

func (a *Admin) hashPassword() error {
	if a.Password != "" && !util.IsStringPrefixInSlice(a.Password, internalHashPwdPrefixes) {
		if config.PasswordValidation.Admins.MinEntropy > 0 {
			if err := passwordvalidator.Validate(a.Password, config.PasswordValidation.Admins.MinEntropy); err != nil {
				return util.NewValidationError(err.Error())
			}
		}
		if config.PasswordHashing.Algo == HashingAlgoBcrypt {
			pwd, err := bcrypt.GenerateFromPassword([]byte(a.Password), config.PasswordHashing.BcryptOptions.Cost)
			if err != nil {
				return err
			}
			a.Password = string(pwd)
		} else {
			pwd, err := argon2id.CreateHash(a.Password, argon2Params)
			if err != nil {
				return err
			}
			a.Password = pwd
		}
	}
	return nil
}

func (a *Admin) hasRedactedSecret() bool {
	return a.Filters.TOTPConfig.Secret.IsRedacted()
}

func (a *Admin) validateRecoveryCodes() error {
	for i := 0; i < len(a.Filters.RecoveryCodes); i++ {
		code := &a.Filters.RecoveryCodes[i]
		if code.Secret.IsEmpty() {
			return util.NewValidationError("mfa: recovery code cannot be empty")
		}
		if code.Secret.IsPlain() {
			code.Secret.SetAdditionalData(a.Username)
			if err := code.Secret.Encrypt(); err != nil {
				return util.NewValidationError(fmt.Sprintf("mfa: unable to encrypt recovery code: %v", err))
			}
		}
	}
	return nil
}

func (a *Admin) validatePermissions() error {
	a.Permissions = util.RemoveDuplicates(a.Permissions)
	if len(a.Permissions) == 0 {
		return util.NewValidationError("please grant some permissions to this admin")
	}
	if util.IsStringInSlice(PermAdminAny, a.Permissions) {
		a.Permissions = []string{PermAdminAny}
	}
	for _, perm := range a.Permissions {
		if !util.IsStringInSlice(perm, validAdminPerms) {
			return util.NewValidationError(fmt.Sprintf("invalid permission: %#v", perm))
		}
	}
	return nil
}

func (a *Admin) validate() error {
	a.SetEmptySecretsIfNil()
	if a.Username == "" {
		return util.NewValidationError("username is mandatory")
	}
	if a.Password == "" {
		return util.NewValidationError("please set a password")
	}
	if a.hasRedactedSecret() {
		return util.NewValidationError("cannot save an admin with a redacted secret")
	}
	if err := a.Filters.TOTPConfig.validate(a.Username); err != nil {
		return err
	}
	if err := a.validateRecoveryCodes(); err != nil {
		return err
	}
	if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) {
		return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username))
	}
	if err := a.hashPassword(); err != nil {
		return err
	}
	if err := a.validatePermissions(); err != nil {
		return err
	}
	if a.Email != "" && !emailRegex.MatchString(a.Email) {
		return util.NewValidationError(fmt.Sprintf("email %#v is not valid", a.Email))
	}
	a.Filters.AllowList = util.RemoveDuplicates(a.Filters.AllowList)
	for _, IPMask := range a.Filters.AllowList {
		_, _, err := net.ParseCIDR(IPMask)
		if err != nil {
			return util.NewValidationError(fmt.Sprintf("could not parse allow list entry %#v : %v", IPMask, err))
		}
	}

	return nil
}

// CheckPassword verifies the admin password
func (a *Admin) CheckPassword(password string) (bool, error) {
	if strings.HasPrefix(a.Password, bcryptPwdPrefix) {
		if err := bcrypt.CompareHashAndPassword([]byte(a.Password), []byte(password)); err != nil {
			return false, ErrInvalidCredentials
		}
		return true, nil
	}
	match, err := argon2id.ComparePasswordAndHash(password, a.Password)
	if !match || err != nil {
		return false, ErrInvalidCredentials
	}
	return match, err
}

// CanLoginFromIP returns true if login from the given IP is allowed
func (a *Admin) CanLoginFromIP(ip string) bool {
	if len(a.Filters.AllowList) == 0 {
		return true
	}
	parsedIP := net.ParseIP(ip)
	if parsedIP == nil {
		return len(a.Filters.AllowList) == 0
	}

	for _, ipMask := range a.Filters.AllowList {
		_, network, err := net.ParseCIDR(ipMask)
		if err != nil {
			continue
		}
		if network.Contains(parsedIP) {
			return true
		}
	}
	return false
}

// CanLogin returns an error if the login is not allowed
func (a *Admin) CanLogin(ip string) error {
	if a.Status != 1 {
		return fmt.Errorf("admin %#v is disabled", a.Username)
	}
	if !a.CanLoginFromIP(ip) {
		return fmt.Errorf("login from IP %v not allowed", ip)
	}
	return nil
}

func (a *Admin) checkUserAndPass(password, ip string) error {
	if err := a.CanLogin(ip); err != nil {
		return err
	}
	if a.Password == "" || password == "" {
		return errors.New("credentials cannot be null or empty")
	}
	match, err := a.CheckPassword(password)
	if err != nil {
		return err
	}
	if !match {
		return ErrInvalidCredentials
	}
	return nil
}

// RenderAsJSON implements the renderer interface used within plugins
func (a *Admin) RenderAsJSON(reload bool) ([]byte, error) {
	if reload {
		admin, err := provider.adminExists(a.Username)
		if err != nil {
			providerLog(logger.LevelWarn, "unable to reload admin before rendering as json: %v", err)
			return nil, err
		}
		admin.HideConfidentialData()
		return json.Marshal(admin)
	}
	a.HideConfidentialData()
	return json.Marshal(a)
}

// HideConfidentialData hides admin confidential data
func (a *Admin) HideConfidentialData() {
	a.Password = ""
	if a.Filters.TOTPConfig.Secret != nil {
		a.Filters.TOTPConfig.Secret.Hide()
	}
	for _, code := range a.Filters.RecoveryCodes {
		if code.Secret != nil {
			code.Secret.Hide()
		}
	}
	a.SetNilSecretsIfEmpty()
}

// SetEmptySecretsIfNil sets the secrets to empty if nil
func (a *Admin) SetEmptySecretsIfNil() {
	if a.Filters.TOTPConfig.Secret == nil {
		a.Filters.TOTPConfig.Secret = kms.NewEmptySecret()
	}
}

// SetNilSecretsIfEmpty set the secrets to nil if empty.
// This is useful before rendering as JSON so the empty fields
// will not be serialized.
func (a *Admin) SetNilSecretsIfEmpty() {
	if a.Filters.TOTPConfig.Secret != nil && a.Filters.TOTPConfig.Secret.IsEmpty() {
		a.Filters.TOTPConfig.Secret = nil
	}
}

// HasPermission returns true if the admin has the specified permission
func (a *Admin) HasPermission(perm string) bool {
	if util.IsStringInSlice(PermAdminAny, a.Permissions) {
		return true
	}
	return util.IsStringInSlice(perm, a.Permissions)
}

// GetPermissionsAsString returns permission as string
func (a *Admin) GetPermissionsAsString() string {
	return strings.Join(a.Permissions, ", ")
}

// GetAllowedIPAsString returns the allowed IP as comma separated string
func (a *Admin) GetAllowedIPAsString() string {
	return strings.Join(a.Filters.AllowList, ",")
}

// GetValidPerms returns the allowed admin permissions
func (a *Admin) GetValidPerms() []string {
	return validAdminPerms
}

// GetInfoString returns admin's info as string.
func (a *Admin) GetInfoString() string {
	var result strings.Builder
	if a.Email != "" {
		result.WriteString(fmt.Sprintf("Email: %v. ", a.Email))
	}
	if len(a.Filters.AllowList) > 0 {
		result.WriteString(fmt.Sprintf("Allowed IP/Mask: %v. ", len(a.Filters.AllowList)))
	}
	return result.String()
}

// CanManageMFA returns true if the admin can add a multi-factor authentication configuration
func (a *Admin) CanManageMFA() bool {
	return len(mfa.GetAvailableTOTPConfigs()) > 0
}

// GetSignature returns a signature for this admin.
// It could change after an update
func (a *Admin) GetSignature() string {
	data := []byte(a.Username)
	data = append(data, []byte(a.Password)...)
	signature := sha256.Sum256(data)
	return base64.StdEncoding.EncodeToString(signature[:])
}

func (a *Admin) getACopy() Admin {
	a.SetEmptySecretsIfNil()
	permissions := make([]string, len(a.Permissions))
	copy(permissions, a.Permissions)
	filters := AdminFilters{}
	filters.AllowList = make([]string, len(a.Filters.AllowList))
	filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth
	filters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled
	filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName
	filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone()
	copy(filters.AllowList, a.Filters.AllowList)
	filters.RecoveryCodes = make([]sdk.RecoveryCode, 0)
	for _, code := range a.Filters.RecoveryCodes {
		if code.Secret == nil {
			code.Secret = kms.NewEmptySecret()
		}
		filters.RecoveryCodes = append(filters.RecoveryCodes, sdk.RecoveryCode{
			Secret: code.Secret.Clone(),
			Used:   code.Used,
		})
	}

	return Admin{
		ID:             a.ID,
		Status:         a.Status,
		Username:       a.Username,
		Password:       a.Password,
		Email:          a.Email,
		Permissions:    permissions,
		Filters:        filters,
		AdditionalInfo: a.AdditionalInfo,
		Description:    a.Description,
		LastLogin:      a.LastLogin,
		CreatedAt:      a.CreatedAt,
		UpdatedAt:      a.UpdatedAt,
	}
}

func (a *Admin) setFromEnv() error {
	envUsername := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_USERNAME"))
	envPassword := strings.TrimSpace(os.Getenv("SFTPGO_DEFAULT_ADMIN_PASSWORD"))
	if envUsername == "" || envPassword == "" {
		return errors.New(`to create the default admin you need to set the env vars "SFTPGO_DEFAULT_ADMIN_USERNAME" and "SFTPGO_DEFAULT_ADMIN_PASSWORD"`)
	}
	a.Username = envUsername
	a.Password = envPassword
	a.Status = 1
	a.Permissions = []string{PermAdminAny}
	return nil
}
