package biz

import (
	"context"
	"errors"
	"fmt"
	"time"
)

var (
	ErrAccountLocked  = errors.New("account locked")
	ErrCaptchaInvalid = errors.New("captcha invalid")
	ErrTokenIssue     = errors.New("token issue failed")
	ErrLoginState     = errors.New("login state failed")
)

type UserDisabledError struct{ UserID uint }

func (e *UserDisabledError) Error() string { return ErrUserDisabled.Error() }
func (e *UserDisabledError) Unwrap() error { return ErrUserDisabled }

type AccountLockedError struct{ Minutes int }

func (e *AccountLockedError) Error() string { return ErrAccountLocked.Error() }
func (e *AccountLockedError) Unwrap() error { return ErrAccountLocked }

type LoginAttempt struct {
	Username  string
	Password  string
	CaptchaID string
	Captcha   string
	IP        string
	Agent     string
}

type AuthenticationResult struct {
	User               *User
	Token              string
	ExpiresAt          time.Time
	NeedChangePassword bool
}

type TokenAuthentication struct {
	Claims    *AuthClaims
	Refreshed *IssuedToken
}

type AuthenticationUsecase struct {
	users    *UserUsecase
	security *SecurityUsecase
	issuer   TokenIssuer
	audit    AuditRecordRepo
}

func NewAuthenticationUsecase(users *UserUsecase, security *SecurityUsecase, issuer TokenIssuer, audit AuditRecordRepo) *AuthenticationUsecase {
	return &AuthenticationUsecase{users: users, security: security, issuer: issuer, audit: audit}
}

func (uc *AuthenticationUsecase) recordLogin(ctx context.Context, attempt *LoginAttempt, status bool, message string, userID uint) {
	_ = uc.audit.RecordLogin(ctx, &LoginLog{Username: attempt.Username, IP: attempt.IP, Status: status, ErrorMessage: message, Agent: attempt.Agent, UserID: userID})
}

func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttempt) (*AuthenticationResult, error) {
	config, err := uc.security.Current(ctx)
	if err != nil || config == nil {
		return nil, fmt.Errorf("%w: security config unavailable", ErrLoginState)
	}
	if config != nil && config.LockEnable {
		locked, lockErr := uc.security.LoginLocked(ctx, attempt.Username)
		if lockErr != nil {
			return nil, fmt.Errorf("%w: %v", ErrLoginState, lockErr)
		}
		if locked {
			uc.recordLogin(ctx, attempt, false, "账号已锁定", 0)
			return nil, &AccountLockedError{Minutes: config.LockDuration}
		}
	}

	ipTTL := time.Hour
	requireCaptcha := config == nil || config.CaptchaOpen == 0
	if config != nil {
		if config.CaptchaTimeout > 0 {
			ipTTL = time.Duration(config.CaptchaTimeout) * time.Second
		}
		failures, counterErr := uc.security.EnsureLoginIPCounter(ctx, attempt.IP, ipTTL)
		if counterErr != nil {
			return nil, fmt.Errorf("%w: %v", ErrLoginState, counterErr)
		}
		requireCaptcha = config.CaptchaOpen == 0 || failures > config.CaptchaOpen
	}
	if requireCaptcha && !uc.security.VerifyCaptcha(ctx, attempt.CaptchaID, attempt.Captcha, true) {
		_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
		uc.recordLogin(ctx, attempt, false, "验证码错误", 0)
		return nil, ErrCaptchaInvalid
	}

	user, err := uc.users.Login(ctx, attempt.Username, attempt.Password)
	if err != nil {
		_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
		if config != nil && config.LockEnable {
			lockTTL := time.Duration(config.LockDuration) * time.Minute
			failures, stateErr := uc.security.IncrementLoginFailure(ctx, attempt.Username, lockTTL)
			if stateErr != nil {
				return nil, fmt.Errorf("%w: %v", ErrLoginState, stateErr)
			}
			if int(failures) >= config.LockThreshold {
				if stateErr = uc.security.LockLogin(ctx, attempt.Username, lockTTL); stateErr != nil {
					return nil, fmt.Errorf("%w: %v", ErrLoginState, stateErr)
				}
			}
		}
		uc.recordLogin(ctx, attempt, false, "用户名不存在或者密码错误", 0)
		return nil, ErrInvalidCredentials
	}
	if user.Enable != 1 {
		_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
		uc.recordLogin(ctx, attempt, false, "用户被禁止登录", user.ID)
		return nil, &UserDisabledError{UserID: user.ID}
	}

	uc.security.ClearLoginState(ctx, attempt.Username)
	if config != nil && config.PwdExpireEnable && config.PwdExpireDays > 0 && user.PasswordUpdatedAt != nil && time.Now().After((*user.PasswordUpdatedAt).AddDate(0, 0, config.PwdExpireDays)) {
		user.MustChangePassword = true
	}
	issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrTokenIssue, err)
	}
	// Record a successful credential login immediately after issuing the JWT.
	// Multipoint-session persistence happens afterwards, so a Redis/cache
	// failure must not erase the successful-login audit event.
	uc.recordLogin(ctx, attempt, true, "登录成功", user.ID)
	if uc.security.UseMultipoint() {
		oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username)
		if cacheErr != nil {
			return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
		}
		if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil {
			if errors.Is(cacheErr, ErrTokenRevoke) {
				return nil, cacheErr
			}
			return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr)
		}
	}
	return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil
}

func userHasAuthority(user *User, authorityID uint) bool {
	if user == nil || authorityID == 0 {
		return false
	}
	if user.AuthorityID == authorityID {
		return true
	}
	for _, authority := range user.Authorities {
		if authority.AuthorityID == authorityID {
			return true
		}
	}
	return false
}

func (uc *AuthenticationUsecase) currentTokenUser(ctx context.Context, claims *AuthClaims) (*User, error) {
	if claims == nil || claims.UUID == "" || claims.IssuedAt.IsZero() {
		return nil, ErrTokenDisabled
	}
	user, err := uc.users.UserByUUID(ctx, claims.UUID)
	if err != nil || user == nil || user.ID != claims.ID || user.Username != claims.Username {
		return nil, ErrTokenDisabled
	}
	if user.Enable != 1 {
		return nil, &UserDisabledError{UserID: user.ID}
	}
	if !userHasAuthority(user, claims.AuthorityID) {
		return nil, ErrTokenDisabled
	}
	passwordVersion := int64(0)
	if user.PasswordUpdatedAt != nil {
		passwordVersion = user.PasswordUpdatedAt.UnixNano()
	}
	if passwordVersion != claims.PasswordVersion {
		return nil, ErrTokenDisabled
	}
	config, err := uc.security.Current(ctx)
	if err != nil || config == nil {
		return nil, ErrTokenDisabled
	}
	if config.PwdExpireEnable && config.PwdExpireDays > 0 && user.PasswordUpdatedAt != nil && time.Now().After(user.PasswordUpdatedAt.AddDate(0, 0, config.PwdExpireDays)) {
		user.MustChangePassword = true
	}
	return user, nil
}

// SwitchAuthority changes the current user's active role and re-signs the
// current claims. Role switching keeps the original JWT expiry; it
// does not treat a role switch as a fresh login or rotate the multipoint
// session. Keeping that distinction is important to the admin UI, which
// replaces the token returned in the response but expects its original TTL.
func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, claims *AuthClaims, authorityID uint) (*AuthenticationResult, error) {
	if claims == nil {
		return nil, ErrInvalidCredentials
	}
	if err := uc.users.SetUserAuthority(ctx, claims.ID, authorityID); err != nil {
		return nil, err
	}
	// Role switching signs the current claims again after replacing only the
	// authority ID. It must not reload mutable profile fields or reset JWT
	// timing/buffer claims as if this were a fresh login.
	issued, err := uc.issuer.ReissueToken(claims, authorityID)
	if err != nil {
		return nil, err
	}
	user := &User{ID: claims.ID, UUID: claims.UUID, Username: claims.Username, NickName: claims.NickName, AuthorityID: authorityID, MustChangePassword: claims.MustChangePwd}
	return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: claims.MustChangePwd}, nil
}

func (uc *AuthenticationUsecase) AuthenticateToken(ctx context.Context, token string) (*TokenAuthentication, error) {
	// The blacklist is checked before parsing the JWT. Besides avoiding work,
	// this makes a revoked-but-expired token report the revocation reason (and
	// not the generic expiry message), which the frontend uses to decide whether
	// to clear a session.
	disabled, err := uc.security.tokens.IsTokenDisabled(ctx, token)
	if err != nil || disabled {
		return nil, ErrTokenDisabled
	}
	claims, err := uc.issuer.ParseToken(token)
	if err != nil {
		return nil, err
	}
	user, err := uc.currentTokenUser(ctx, claims)
	if err != nil {
		return nil, err
	}
	claims.NickName = user.NickName
	claims.MustChangePwd = user.MustChangePassword
	result := &TokenAuthentication{Claims: claims}
	if claims.BufferTime <= 0 || time.Until(claims.ExpiresAt) >= claims.BufferTime {
		return result, nil
	}
	issued, err := uc.issuer.IssueToken(user, claims.AuthorityID, user.MustChangePassword, 0)
	if err != nil {
		return result, nil
	}
	// Refreshing updates the active Redis token but does not blacklist the old JWT;
	// both tokens remain valid until their own expiry. Passing an empty old token
	// preserves that behavior while still updating the multipoint session.
	if err = uc.security.RotateActiveToken(ctx, claims.Username, "", issued.Value, issued.TTL); err != nil {
		return result, nil
	}
	result.Refreshed = issued
	return result, nil
}
