package system

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"
	"time"
	"unicode"
	"unicode/utf8"
)

type SecurityConfig struct {
	ID                         uint
	CreatedAt                  time.Time
	UpdatedAt                  time.Time
	CaptchaOpen                int
	CaptchaTimeout             int
	KeyLong                    int
	ImgWidth                   int
	ImgHeight                  int
	PwdMinLength               int
	PwdRequireUpper            bool
	PwdRequireLower            bool
	PwdRequireDigit            bool
	PwdRequireSpecial          bool
	LimitEnable                bool
	LimitWindow                int
	LimitCount                 int
	LockEnable                 bool
	LockThreshold              int
	LockDuration               int
	PwdExpireEnable            bool
	PwdExpireDays              int
	ForceNewUserChangePassword bool
}

type SecurityRepo interface {
	SecurityConfig(context.Context) (*SecurityConfig, error)
	SaveSecurityConfig(context.Context, *SecurityConfig) error
	BackfillPasswordUpdatedAt(context.Context, time.Time) error
}

type SecurityUsecase struct {
	repo         SecurityRepo
	cache        Cache
	settings     RuntimeSettings
	tokens       *TokenUsecase
	mu           sync.RWMutex
	cachedConfig *SecurityConfig
}

var ErrTokenRevoke = errors.New("token revoke failed")

type TokenRevokeError struct{ Err error }

func (e *TokenRevokeError) Error() string        { return e.Err.Error() }
func (e *TokenRevokeError) Is(target error) bool { return target == ErrTokenRevoke }

func NewSecurityUsecase(repo SecurityRepo, cache Cache, settings RuntimeSettings, tokens *TokenUsecase) *SecurityUsecase {
	return &SecurityUsecase{repo: repo, cache: cache, settings: settings, tokens: tokens}
}

func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error {
	previous, err := uc.repo.SecurityConfig(ctx)
	if err != nil {
		return err
	}
	value.ID = previous.ID
	value.CreatedAt = previous.CreatedAt
	value.UpdatedAt = previous.UpdatedAt
	if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil {
		return err
	}
	uc.mu.Lock()
	copy := *value
	uc.cachedConfig = &copy
	uc.mu.Unlock()
	// Keep the same observable ordering as the administration backend:
	// persist and activate the new configuration first, then backfill legacy
	// users when password expiration changes from disabled to enabled. A
	// backfill failure is returned to the caller without rolling back the
	// already effective configuration.
	if value.PwdExpireEnable && !previous.PwdExpireEnable {
		return uc.repo.BackfillPasswordUpdatedAt(ctx, time.Now())
	}
	return nil
}

func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error) {
	uc.mu.RLock()
	if uc.cachedConfig != nil {
		copy := *uc.cachedConfig
		uc.mu.RUnlock()
		return &copy, nil
	}
	uc.mu.RUnlock()
	value, err := uc.repo.SecurityConfig(ctx)
	if err != nil {
		// Return the default value together with the database-not-ready
		// error. Callers such as login/runtime policy consumers intentionally
		// ignore the error and continue with that default, while the HTTP
		// settings endpoint still reports the failure.
		if value != nil {
			return value, err
		}
		return nil, err
	}
	uc.mu.Lock()
	copy := *value
	uc.cachedConfig = &copy
	uc.mu.Unlock()
	return value, nil
}

// Security returns the persisted row for the administration endpoint. Runtime
// policy consumers use Current so their hot path remains cached, while the
// settings page mirrors the reference behavior and always observes storage.
func (uc *SecurityUsecase) Security(ctx context.Context) (*SecurityConfig, error) {
	return uc.repo.SecurityConfig(ctx)
}

func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password string) error {
	if value.PwdMinLength > 0 && utf8.RuneCountInString(password) < value.PwdMinLength {
		return fmt.Errorf("密码长度不能少于 %d 位", value.PwdMinLength)
	}
	hasUpper, hasLower, hasDigit, hasSpecial := false, false, false, false
	for _, ch := range password {
		switch {
		case unicode.IsUpper(ch):
			hasUpper = true
		case unicode.IsLower(ch):
			hasLower = true
		case unicode.IsDigit(ch):
			hasDigit = true
		case unicode.IsPunct(ch) || unicode.IsSymbol(ch):
			hasSpecial = true
		}
	}
	missing := make([]string, 0, 4)
	if value.PwdRequireUpper && !hasUpper {
		missing = append(missing, "大写字母")
	}
	if value.PwdRequireLower && !hasLower {
		missing = append(missing, "小写字母")
	}
	if value.PwdRequireDigit && !hasDigit {
		missing = append(missing, "数字")
	}
	if value.PwdRequireSpecial && !hasSpecial {
		missing = append(missing, "特殊字符")
	}
	if len(missing) > 0 {
		return fmt.Errorf("密码必须包含%s", strings.Join(missing, "、"))
	}
	return nil
}

func (uc *SecurityUsecase) ValidateCurrentPassword(ctx context.Context, password string) error {
	config, err := uc.Current(ctx)
	if err != nil {
		return err
	}
	return uc.ValidatePassword(config, password)
}

func (uc *SecurityUsecase) LoginLocked(ctx context.Context, username string) (bool, error) {
	_, locked, err := uc.cache.Get(ctx, loginLockKey(username))
	return locked, err
}

func (uc *SecurityUsecase) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) {
	return uc.cache.Increment(ctx, loginFailureKey(username), expiration)
}

func (uc *SecurityUsecase) LockLogin(ctx context.Context, username string, expiration time.Duration) error {
	return uc.cache.Set(ctx, loginLockKey(username), "1", expiration)
}

func (uc *SecurityUsecase) ClearLoginState(ctx context.Context, username string) {
	_ = uc.cache.Delete(ctx, loginFailureKey(username))
	_ = uc.cache.Delete(ctx, loginLockKey(username))
}

func (uc *SecurityUsecase) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) {
	value, exists, err := uc.cache.Get(ctx, ip)
	if err != nil {
		return 0, err
	}
	if exists {
		return strconv.Atoi(value)
	}
	if expiration <= 0 {
		expiration = time.Hour
	}
	if err = uc.cache.Set(ctx, ip, "1", expiration); err != nil {
		return 0, err
	}
	return 1, nil
}

func (uc *SecurityUsecase) IncrementLoginIP(ctx context.Context, ip string, expiration time.Duration) (int64, error) {
	return uc.cache.Increment(ctx, ip, expiration)
}

func (uc *SecurityUsecase) IncrementRateLimit(ctx context.Context, key string, expiration time.Duration) (int64, error) {
	return uc.cache.Increment(ctx, key, expiration)
}

func (uc *SecurityUsecase) SetCaptcha(ctx context.Context, id, value string, expiration time.Duration) error {
	return uc.cache.Set(ctx, "CAPTCHA_"+id, value, expiration)
}

func (uc *SecurityUsecase) GetCaptcha(ctx context.Context, id string) (string, bool, error) {
	return uc.cache.Get(ctx, "CAPTCHA_"+id)
}

func (uc *SecurityUsecase) DeleteCaptcha(ctx context.Context, id string) error {
	return uc.cache.Delete(ctx, "CAPTCHA_"+id)
}

func (uc *SecurityUsecase) VerifyCaptcha(ctx context.Context, id, answer string, clear bool) bool {
	if id == "" || answer == "" {
		return false
	}
	value, ok, err := uc.GetCaptcha(ctx, id)
	if err != nil || !ok {
		return false
	}
	if clear {
		_ = uc.DeleteCaptcha(ctx, id)
	}
	return strings.EqualFold(value, answer)
}

// The currently active multipoint token is stored under the username itself.
// Keep the same key so a rolling deployment can revoke sessions issued by the
// other implementation instead of silently creating a second active session.
func activeTokenKey(username string) string { return username }

func loginFailureKey(username string) string { return "login_fail:" + username }
func loginLockKey(username string) string    { return "login_lock:" + username }

func (uc *SecurityUsecase) ActiveToken(ctx context.Context, username string) (string, error) {
	value, _, err := uc.cache.Get(ctx, activeTokenKey(username))
	return value, err
}

func (uc *SecurityUsecase) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) {
	if !uc.settings.UseMultipoint() {
		return true, nil
	}
	active, ok, err := uc.cache.Get(ctx, activeTokenKey(username))
	return ok && active == token, err
}

func (uc *SecurityUsecase) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error {
	if !uc.settings.UseMultipoint() {
		return nil
	}
	if oldToken != "" && oldToken != newToken {
		if err := uc.tokens.BlacklistToken(ctx, oldToken); err != nil {
			return &TokenRevokeError{Err: err}
		}
	}
	return uc.cache.Set(ctx, activeTokenKey(username), newToken, expiration)
}

func (uc *SecurityUsecase) UseMultipoint() bool { return uc.settings.UseMultipoint() }

func (uc *SecurityUsecase) CaptchaRuntimeSettings() CaptchaSettings {
	return uc.settings.CaptchaSettings()
}
