package identity

import (
	"context"
	"crypto/rand"
	"errors"
	"fmt"
	"math/big"
	"strings"
	"time"

	goredis "github.com/redis/go-redis/v9"

	redisstore "qycw/go-server/internal/redis"
)

const (
	sendCooldown      = 60 * time.Second
	codeTTL           = 10 * time.Minute
	verifyMaxAttempts = 5
)

func generateCode() (string, error) {
	n, err := rand.Int(rand.Reader, big.NewInt(1_000_000))
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%06d", n.Int64()), nil
}

func codeTarget(target string) string {
	return strings.ToLower(strings.TrimSpace(target))
}

func codeKey(purpose string, target string) string {
	return redisstore.Key("verification_code", strings.ToLower(strings.TrimSpace(purpose)), codeTarget(target))
}

func cooldownKey(purpose string, target string) string {
	return redisstore.Key("verification_code_cooldown", strings.ToLower(strings.TrimSpace(purpose)), codeTarget(target))
}

func attemptKey(purpose string, target string) string {
	return redisstore.Key("verification_code_attempts", strings.ToLower(strings.TrimSpace(purpose)), codeTarget(target))
}

func (s *Service) canSend(ctx context.Context, purpose string, target string) (int, error) {
	if s.redis == nil {
		return 0, ServiceUnavailable("Redis 未启用")
	}
	ttl, err := s.redis.TTL(ctx, cooldownKey(purpose, target)).Result()
	if err != nil {
		return 0, err
	}
	if ttl > 0 {
		return int(ttl.Seconds()), nil
	}
	return 0, nil
}

func (s *Service) saveCode(ctx context.Context, purpose string, target string, code string) error {
	if s.redis == nil {
		return ServiceUnavailable("Redis 未启用")
	}
	if err := s.redis.Set(ctx, codeKey(purpose, target), code, codeTTL).Err(); err != nil {
		return err
	}
	if err := s.redis.Set(ctx, cooldownKey(purpose, target), "1", sendCooldown).Err(); err != nil {
		return err
	}
	return s.redis.Del(ctx, attemptKey(purpose, target)).Err()
}

func (s *Service) verifyCode(ctx context.Context, purpose string, target string, code string) (bool, error) {
	if s.redis == nil {
		return false, ServiceUnavailable("Redis 未启用")
	}

	storedCodeKey := codeKey(purpose, target)
	stored, err := s.redis.Get(ctx, storedCodeKey).Result()
	if err != nil {
		if errors.Is(err, goredis.Nil) {
			return false, nil
		}
		return false, err
	}

	attemptsKey := attemptKey(purpose, target)
	if strings.TrimSpace(stored) == strings.TrimSpace(code) {
		return true, s.redis.Del(ctx, storedCodeKey, attemptsKey).Err()
	}

	attempts, err := s.redis.Incr(ctx, attemptsKey).Result()
	if err != nil {
		return false, err
	}
	if attempts == 1 {
		_ = s.redis.Expire(ctx, attemptsKey, codeTTL).Err()
	}
	if attempts >= verifyMaxAttempts {
		_ = s.redis.Del(ctx, storedCodeKey, attemptsKey).Err()
	}
	return false, nil
}
