package admin

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

	"github.com/redis/go-redis/v9"
	"github.com/wangjia/pangolin/server/internal/totp"
)

// Login outcome sentinel errors.
var (
	// ErrInvalidCredentials is returned for any wrong username / password /
	// TOTP combination. It is deliberately generic to avoid user enumeration.
	ErrInvalidCredentials = errors.New("admin: invalid credentials")
	// ErrLockedOut is returned when the username is temporarily locked after
	// too many consecutive failures.
	ErrLockedOut = errors.New("admin: account temporarily locked")
)

const loginFailKeyPrefix = "admin:loginfail:"

// Authenticator performs two-factor admin login with failure rate-limiting.
type Authenticator struct {
	store    Store
	sessions *SessionStore
	rdb      *redis.Client
	secret   []byte
	failMax  int
	lockDur  time.Duration
	sec      *SecurityLog
	// now is overridable in tests.
	now func() time.Time
}

// NewAuthenticator wires an Authenticator.
func NewAuthenticator(store Store, sessions *SessionStore, rdb *redis.Client, cfg *Config, sec *SecurityLog) *Authenticator {
	return &Authenticator{
		store:    store,
		sessions: sessions,
		rdb:      rdb,
		secret:   cfg.SecretKey,
		failMax:  cfg.LoginFailMax,
		lockDur:  cfg.LoginLockDuration,
		sec:      sec,
		now:      func() time.Time { return time.Now().UTC() },
	}
}

// Login validates username + password + TOTP and, on success, creates a
// session and returns its id. Every failure is rate-limited and recorded as a
// security event in the audit log (red line: admin records only security
// events, never routine access).
func (a *Authenticator) Login(ctx context.Context, username, password, code, remoteIP string) (sid string, sess *Session, err error) {
	locked, lerr := a.isLocked(ctx, username)
	if lerr != nil {
		return "", nil, fmt.Errorf("admin.Login lock check: %w", lerr)
	}
	if locked {
		a.sec.LoginLocked(ctx, username, remoteIP)
		return "", nil, ErrLockedOut
	}

	admin, gerr := a.store.GetAdminByUsername(ctx, username)
	if gerr != nil && !errors.Is(gerr, ErrAdminNotFound) {
		return "", nil, fmt.Errorf("admin.Login lookup: %w", gerr)
	}

	if admin == nil || admin.Status != "active" || !VerifyPassword(admin.PwHash, password) {
		a.recordFail(ctx, username)
		a.sec.LoginFail(ctx, username, remoteIP, "bad_password")
		return "", nil, ErrInvalidCredentials
	}

	secret, derr := DecryptSecret(a.secret, admin.TOTPSecretEnc)
	if derr != nil {
		a.recordFail(ctx, username)
		a.sec.LoginFail(ctx, username, remoteIP, "totp_decrypt")
		return "", nil, ErrInvalidCredentials
	}
	if !totp.Validate(secret, code, a.now(), 1) {
		a.recordFail(ctx, username)
		a.sec.LoginFail(ctx, username, remoteIP, "bad_totp")
		return "", nil, ErrInvalidCredentials
	}

	// Success: clear counter, stamp login, create session.
	a.clearFail(ctx, username)
	if uerr := a.store.UpdateLastLogin(ctx, admin.ID, a.now()); uerr != nil {
		return "", nil, fmt.Errorf("admin.Login update: %w", uerr)
	}
	sid, sess, serr := a.sessions.Create(ctx, admin.ID, admin.Username)
	if serr != nil {
		return "", nil, serr
	}
	a.sec.LoginOK(ctx, username, remoteIP)
	return sid, sess, nil
}

func (a *Authenticator) isLocked(ctx context.Context, username string) (bool, error) {
	if a.rdb == nil {
		return false, nil
	}
	n, err := a.rdb.Get(ctx, loginFailKeyPrefix+username).Int()
	if errors.Is(err, redis.Nil) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return n >= a.failMax, nil
}

func (a *Authenticator) recordFail(ctx context.Context, username string) {
	if a.rdb == nil {
		return
	}
	key := loginFailKeyPrefix + username
	pipe := a.rdb.Pipeline()
	pipe.Incr(ctx, key)
	pipe.Expire(ctx, key, a.lockDur)
	_, _ = pipe.Exec(ctx)
}

func (a *Authenticator) clearFail(ctx context.Context, username string) {
	if a.rdb == nil {
		return
	}
	_ = a.rdb.Del(ctx, loginFailKeyPrefix+username).Err()
}
