package service

import (
	"context"
	cryptorand "crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"fmt"
	"log"
	"net/smtp"
	"net/url"
	"strings"
	"time"
)

type EmailProvider interface {
	SendMail(ctx context.Context, to, subject, htmlBody string) error
}

type SMTPEmailConfig struct {
	Host      string
	Port      int
	Username  string
	Password  string
	FromEmail string
	FromName  string
	TLS       bool
}

type SMTPEmailProvider struct {
	cfg SMTPEmailConfig
}

func NewSMTPEmailProvider(cfg SMTPEmailConfig) EmailProvider {
	return &SMTPEmailProvider{cfg: cfg}
}

func (p *SMTPEmailProvider) SendMail(ctx context.Context, to, subject, htmlBody string) error {
	_ = ctx

	var authInfo smtp.Auth
	if p.cfg.Username != "" || p.cfg.Password != "" {
		authInfo = smtp.PlainAuth("", p.cfg.Username, p.cfg.Password, p.cfg.Host)
	}

	from := p.cfg.FromEmail
	if p.cfg.FromName != "" {
		from = fmt.Sprintf("%s <%s>", p.cfg.FromName, p.cfg.FromEmail)
	}

	headers := []string{
		fmt.Sprintf("From: %s", from),
		fmt.Sprintf("To: %s", to),
		fmt.Sprintf("Subject: %s", subject),
		"MIME-Version: 1.0",
		"Content-Type: text/html; charset=UTF-8",
		"",
	}

	message := strings.Join(headers, "\r\n") + htmlBody
	addr := fmt.Sprintf("%s:%d", p.cfg.Host, p.cfg.Port)
	return smtp.SendMail(addr, authInfo, p.cfg.FromEmail, []string{to}, []byte(message))
}

type MockEmailProvider struct{}

func (m *MockEmailProvider) SendMail(ctx context.Context, to, subject, htmlBody string) error {
	_ = ctx
	log.Printf("[email-mock] to=%s subject=%s body_bytes=%d", to, subject, len(htmlBody))
	return nil
}

type EmailCodeConfig struct {
	CodeTTL        time.Duration
	ResendCooldown time.Duration
	MaxDailyLimit  int
	SiteURL        string
	SiteName       string
}

func DefaultEmailCodeConfig() EmailCodeConfig {
	return EmailCodeConfig{
		CodeTTL:        5 * time.Minute,
		ResendCooldown: time.Minute,
		MaxDailyLimit:  10,
		SiteURL:        "http://localhost:8080",
		SiteName:       "User Management System",
	}
}

type EmailCodeService struct {
	provider EmailProvider
	cache    cacheInterface
	cfg      EmailCodeConfig
}

func NewEmailCodeService(provider EmailProvider, cache cacheInterface, cfg EmailCodeConfig) *EmailCodeService {
	if cfg.CodeTTL <= 0 {
		cfg.CodeTTL = 5 * time.Minute
	}
	if cfg.ResendCooldown <= 0 {
		cfg.ResendCooldown = time.Minute
	}
	if cfg.MaxDailyLimit <= 0 {
		cfg.MaxDailyLimit = 10
	}
	return &EmailCodeService{
		provider: provider,
		cache:    cache,
		cfg:      cfg,
	}
}

func (s *EmailCodeService) SendEmailCode(ctx context.Context, email, purpose string) error {
	cooldownKey := fmt.Sprintf("email_cooldown:%s:%s", purpose, email)
	if _, ok := s.cache.Get(ctx, cooldownKey); ok {
		return newRateLimitError(fmt.Sprintf("\u64cd\u4f5c\u8fc7\u4e8e\u9891\u7e41\uff0c\u8bf7 %d \u79d2\u540e\u518d\u8bd5", int(s.cfg.ResendCooldown.Seconds())))
	}

	dailyKey := fmt.Sprintf("email_daily:%s:%s", email, time.Now().Format("2006-01-02"))
	var dailyCount int
	if value, ok := s.cache.Get(ctx, dailyKey); ok {
		if count, ok := intValue(value); ok {
			dailyCount = count
		}
	}
	if dailyCount >= s.cfg.MaxDailyLimit {
		return newRateLimitError("\u4eca\u65e5\u53d1\u9001\u6b21\u6570\u5df2\u8fbe\u4e0a\u9650\uff0c\u8bf7\u660e\u5929\u518d\u8bd5")
	}

	code, err := generateEmailCode()
	if err != nil {
		return err
	}
	codeKey := fmt.Sprintf("email_code:%s:%s", purpose, email)
	if err := s.cache.Set(ctx, codeKey, code, s.cfg.CodeTTL, s.cfg.CodeTTL); err != nil {
		return fmt.Errorf("store email code failed: %w", err)
	}
	if err := s.cache.Set(ctx, cooldownKey, true, s.cfg.ResendCooldown, s.cfg.ResendCooldown); err != nil {
		_ = s.cache.Delete(ctx, codeKey)
		return fmt.Errorf("store email cooldown failed: %w", err)
	}
	if err := s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour, 24*time.Hour); err != nil {
		_ = s.cache.Delete(ctx, codeKey)
		_ = s.cache.Delete(ctx, cooldownKey)
		return fmt.Errorf("store email daily counter failed: %w", err)
	}

	subject, body := buildEmailCodeContent(purpose, code, s.cfg.SiteName, s.cfg.CodeTTL)
	if err := s.provider.SendMail(ctx, email, subject, body); err != nil {
		_ = s.cache.Delete(ctx, codeKey)
		_ = s.cache.Delete(ctx, cooldownKey)
		return fmt.Errorf("email delivery failed: %w", err)
	}

	return nil
}

func (s *EmailCodeService) VerifyEmailCode(ctx context.Context, email, purpose, code string) error {
	if strings.TrimSpace(code) == "" {
		return fmt.Errorf("verification code is required")
	}

	codeKey := fmt.Sprintf("email_code:%s:%s", purpose, email)
	value, ok := s.cache.Get(ctx, codeKey)
	if !ok {
		return fmt.Errorf("verification code expired or missing")
	}

	storedCode, ok := value.(string)
	if !ok || subtle.ConstantTimeCompare([]byte(storedCode), []byte(code)) != 1 {
		return fmt.Errorf("verification code is invalid")
	}

	if err := s.cache.Delete(ctx, codeKey); err != nil {
		return fmt.Errorf("consume email code failed: %w", err)
	}

	return nil
}

type EmailActivationService struct {
	provider EmailProvider
	cache    cacheInterface
	tokenTTL time.Duration
	siteURL  string
	siteName string
}

func NewEmailActivationService(provider EmailProvider, cache cacheInterface, siteURL, siteName string) *EmailActivationService {
	return &EmailActivationService{
		provider: provider,
		cache:    cache,
		tokenTTL: 24 * time.Hour,
		siteURL:  siteURL,
		siteName: siteName,
	}
}

func (s *EmailActivationService) SendActivationEmail(ctx context.Context, userID int64, email, username string) error {
	tokenBytes := make([]byte, 32)
	if _, err := cryptorand.Read(tokenBytes); err != nil {
		return fmt.Errorf("generate activation token failed: %w", err)
	}
	token := hex.EncodeToString(tokenBytes)

	cacheKey := fmt.Sprintf("email_activation:%s", token)
	if err := s.cache.Set(ctx, cacheKey, userID, s.tokenTTL, s.tokenTTL); err != nil {
		return fmt.Errorf("store activation token failed: %w", err)
	}

	activationURL := buildFrontendActivationURL(s.siteURL, token)
	subject := fmt.Sprintf("[%s] Activate Your Account", s.siteName)
	body := buildActivationEmailBody(username, activationURL, s.siteName, s.tokenTTL)
	return s.provider.SendMail(ctx, email, subject, body)
}

func buildFrontendActivationURL(siteURL, token string) string {
	base := strings.TrimRight(strings.TrimSpace(siteURL), "/")
	if base == "" {
		base = DefaultEmailCodeConfig().SiteURL
	}
	return fmt.Sprintf("%s/activate-account?token=%s", base, url.QueryEscape(token))
}

func (s *EmailActivationService) ValidateActivationToken(ctx context.Context, token string) (int64, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return 0, fmt.Errorf("activation token is required")
	}

	cacheKey := fmt.Sprintf("email_activation:%s", token)
	value, ok := s.cache.Get(ctx, cacheKey)
	if !ok {
		return 0, fmt.Errorf("activation token expired or missing")
	}

	userID, ok := int64Value(value)
	if !ok {
		return 0, fmt.Errorf("activation token payload is invalid")
	}
	if err := s.cache.Delete(ctx, cacheKey); err != nil {
		return 0, fmt.Errorf("consume activation token failed: %w", err)
	}

	return userID, nil
}

func buildEmailCodeContent(purpose, code, siteName string, ttl time.Duration) (subject, body string) {
	purposeText := map[string]string{
		"login":    "login verification",
		"register": "registration verification",
		"reset":    "password reset",
		"bind":     "binding verification",
	}
	label := purposeText[purpose]
	if label == "" {
		label = "identity verification"
	}

	subject = fmt.Sprintf("[%s] Your %s code: %s", siteName, label, code)
	body = fmt.Sprintf(`<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px;">
  <h2 style="color:#333;">%s</h2>
  <p>Your %s code is:</p>
  <div style="background:#f5f5f5;padding:20px;text-align:center;margin:20px 0;border-radius:8px;">
    <span style="font-size:36px;font-weight:bold;color:#2563eb;letter-spacing:8px;">%s</span>
  </div>
  <p>This code expires in <strong>%d minutes</strong>.</p>
  <p style="color:#999;font-size:12px;">If you did not request this code, you can ignore this email.</p>
</body>
</html>`, siteName, label, code, int(ttl.Minutes()))
	return subject, body
}

func buildActivationEmailBody(username, activationURL, siteName string, ttl time.Duration) string {
	return fmt.Sprintf(`<!DOCTYPE html>
<html>
<body style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px;">
  <h2 style="color:#333;">Welcome to %s</h2>
  <p>Hello <strong>%s</strong>,</p>
  <p>Please click the button below to activate your account.</p>
  <div style="text-align:center;margin:30px 0;">
    <a href="%s"
       style="background:#2563eb;color:#fff;padding:14px 32px;text-decoration:none;border-radius:8px;font-size:16px;font-weight:bold;">
      Activate Account
    </a>
  </div>
  <p>If the button does not work, copy this link into your browser:</p>
  <p style="word-break:break-all;color:#2563eb;">%s</p>
  <p>This link expires in <strong>%d hours</strong>.</p>
</body>
</html>`, siteName, username, activationURL, activationURL, int(ttl.Hours()))
}

func generateEmailCode() (string, error) {
	// 使用 6 字节随机数提供足够的熵（48 位）
	buffer := make([]byte, 6)
	if _, err := cryptorand.Read(buffer); err != nil {
		return "", fmt.Errorf("generate email code failed: %w", err)
	}

	value := int(buffer[0])<<40 | int(buffer[1])<<32 | int(buffer[2])<<24 |
		int(buffer[3])<<16 | int(buffer[4])<<8 | int(buffer[5])
	value = value % 1000000
	if value < 100000 {
		value += 100000
	}
	return fmt.Sprintf("%06d", value), nil
}
