package usecase

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

	"apps/backend/internal/module/member/domain"
	tokenDomain "apps/backend/internal/module/token/domain"
)

// AccountService implements domain.AccountUseCase (stand-alone full member surface).
// AuthService is an alias so existing callers keep working.
type AccountService struct {
	Repo       domain.Repository
	Issuer     tokenDomain.Issuer
	BcryptCost int
	// OAuth optional config (empty = dev mock path)
	GoogleClientID   string
	LineClientID     string
	LineClientSecret string
	LineRedirectURI  string
}

// AuthService kept as type alias for svc/logic compatibility.
type AuthService = AccountService

func NewAuthService(repo domain.Repository, issuer tokenDomain.Issuer, bcryptCost int) *AccountService {
	return NewAccountService(repo, issuer, bcryptCost)
}

func NewAccountService(repo domain.Repository, issuer tokenDomain.Issuer, bcryptCost int) *AccountService {
	if bcryptCost <= 0 {
		bcryptCost = 10
	}
	return &AccountService{Repo: repo, Issuer: issuer, BcryptCost: bcryptCost}
}

// ---------- create / login ----------

func (s *AccountService) CreateUserAccount(ctx context.Context, req domain.CreateLoginUserRequest) (*domain.Member, *tokenDomain.TokenPair, error) {
	platform := strings.ToLower(strings.TrimSpace(req.Platform))
	if platform == "" {
		platform = domain.PlatformPassword
	}
	loginID := strings.TrimSpace(req.LoginID)
	if loginID == "" {
		return nil, nil, domain.ErrBadPassword
	}
	if platform == domain.PlatformPassword {
		loginID = strings.ToLower(loginID)
	}

	// already bound?
	if _, err := s.Repo.FindIdentity(ctx, loginID, platform); err == nil {
		return nil, nil, domain.ErrIdentityTaken
	}

	uid, err := s.Repo.NextUID(ctx)
	if err != nil {
		return nil, nil, err
	}
	now := domain.NowNano()
	display := strings.TrimSpace(req.DisplayName)
	email := strings.ToLower(strings.TrimSpace(req.Email))
	phone := strings.TrimSpace(req.Phone)
	if platform == domain.PlatformPassword {
		if req.AccountType == domain.AccountTypePhone || looksLikePhone(loginID) {
			if phone == "" {
				phone = loginID
			}
			req.AccountType = domain.AccountTypePhone
		} else {
			if email == "" {
				email = loginID
			}
			req.AccountType = domain.AccountTypeEmail
		}
	}
	if display == "" {
		if email != "" {
			display = strings.Split(email, "@")[0]
		} else {
			display = loginID
		}
	}

	var passHash string
	if platform == domain.PlatformPassword {
		if req.Password == "" {
			return nil, nil, domain.ErrWeakPassword
		}
		passHash, err = HashPassword(req.Password, s.BcryptCost)
		if err != nil {
			return nil, nil, err
		}
	}

	status := domain.StatusActive
	if platform == domain.PlatformPassword {
		status = domain.StatusUnverified
	}

	m := &domain.Member{
		UID: uid, TenantID: "default", Email: email, Phone: phone,
		DisplayName: display, Roles: []string{domain.RoleMember}, Status: status,
		EmailVerified: false, InviteCode: NewInviteCode(), PasswordHash: passHash,
		Timezone: "Asia/Taipei", PreferredLanguage: "zh-TW", Currency: "TWD",
		OnboardingStatus: domain.OnboardingPending,
		CreatedAt: now, UpdatedAt: now,
	}
	if err := s.Repo.CreateMember(ctx, m); err != nil {
		return nil, nil, err
	}

	accType := req.AccountType
	if accType == "" {
		if platform == domain.PlatformPassword {
			if phone != "" && email == "" {
				accType = domain.AccountTypePhone
			} else {
				accType = domain.AccountTypeEmail
			}
		} else {
			accType = domain.AccountTypePlatform
		}
	}
	ident := &domain.Identity{
		UID: uid, LoginID: loginID, Platform: platform,
		AccountType: accType, PasswordHash: passHash, CreatedAt: now, UpdatedAt: now,
	}
	if err := s.Repo.CreateIdentity(ctx, ident); err != nil {
		return nil, nil, err
	}

	pair, err := s.issueSession(m)
	if err != nil {
		return m, nil, err
	}
	return m, pair, nil
}

func (s *AccountService) LoginPassword(ctx context.Context, loginID, password, platform string) (*domain.Member, *tokenDomain.TokenPair, error) {
	if platform == "" {
		platform = domain.PlatformPassword
	}
	loginID = strings.TrimSpace(loginID)
	if platform == domain.PlatformPassword {
		loginID = strings.ToLower(loginID)
	}

	// Prefer identity; fallback member email for seed admin without identity row
	ident, err := s.Repo.FindIdentity(ctx, loginID, platform)
	var m *domain.Member
	switch {
	case err == nil:
		hash := ident.PasswordHash
		if hash == "" || !CheckPasswordHash(password, hash) {
			// also try member primary password
			m, err = s.Repo.FindByUID(ctx, ident.UID)
			if err != nil {
				return nil, nil, err
			}
			if m.PasswordHash == "" || !CheckPasswordHash(password, m.PasswordHash) {
				return nil, nil, domain.ErrBadPassword
			}
		} else {
			m, err = s.Repo.FindByUID(ctx, ident.UID)
			if err != nil {
				return nil, nil, err
			}
		}
	case errors.Is(err, domain.ErrIdentityNotFound):
		// legacy: email login without identity
		m, err = s.Repo.FindByEmail(ctx, loginID)
		if errors.Is(err, domain.ErrNotFound) {
			return nil, nil, domain.ErrBadPassword
		}
		if err != nil {
			return nil, nil, err // DB/auth errors must not look like bad password
		}
		if m.PasswordHash == "" || !CheckPasswordHash(password, m.PasswordHash) {
			return nil, nil, domain.ErrBadPassword
		}
		// auto-heal identity for seed users
		_ = s.Repo.CreateIdentity(ctx, &domain.Identity{
			UID: m.UID, LoginID: m.Email, Platform: domain.PlatformPassword,
			AccountType: domain.AccountTypeEmail, PasswordHash: m.PasswordHash,
			CreatedAt: domain.NowNano(), UpdatedAt: domain.NowNano(),
		})
	default:
		// infrastructure failure (e.g. Mongo auth)
		return nil, nil, err
	}
	if m.IsSuspended() {
		return nil, nil, domain.ErrSuspended
	}
	pair, err := s.issueSession(m)
	if err != nil {
		return nil, nil, err
	}
	return m, pair, nil
}

// Login — Harbor Desk email login shorthand
func (s *AccountService) Login(ctx context.Context, email, password string) (*domain.Member, *tokenDomain.TokenPair, error) {
	return s.LoginPassword(ctx, email, password, domain.PlatformPassword)
}

func (s *AccountService) LoginOAuth(ctx context.Context, platform, subject, email, displayName string) (*domain.Member, *tokenDomain.TokenPair, error) {
	platform = strings.ToLower(strings.TrimSpace(platform))
	subject = strings.TrimSpace(subject)
	if platform == "" || subject == "" {
		return nil, nil, domain.ErrOAuthFailed
	}
	email = strings.ToLower(strings.TrimSpace(email))

	if ident, err := s.Repo.FindIdentity(ctx, subject, platform); err == nil {
		m, err := s.Repo.FindByUID(ctx, ident.UID)
		if err != nil {
			return nil, nil, err
		}
		if m.IsSuspended() {
			return nil, nil, domain.ErrSuspended
		}
		pair, err := s.issueSession(m)
		return m, pair, err
	}

	// new member via oauth
	req := domain.CreateLoginUserRequest{
		LoginID: subject, Platform: platform, AccountType: domain.AccountTypePlatform,
		DisplayName: displayName, Email: email,
	}
	m, pair, err := s.CreateUserAccount(ctx, req)
	if err != nil {
		return nil, nil, err
	}
	// oauth accounts treated as verified email when provider gives email
	if email != "" {
		now := domain.NowNano()
		m.EmailVerified = true
		m.EmailVerifiedAt = &now
		m.Status = domain.StatusActive
		m.UpdatedAt = now
		_ = s.Repo.UpdateMember(ctx, m)
	}
	return m, pair, nil
}

// ---------- query ----------

func (s *AccountService) GetUIDByAccount(ctx context.Context, account, platform string) (int64, error) {
	ident, err := s.Repo.FindIdentity(ctx, account, platform)
	if err != nil {
		return 0, err
	}
	return ident.UID, nil
}

func (s *AccountService) GetUserAccountInfo(ctx context.Context, account, platform string) (*domain.Identity, error) {
	return s.Repo.FindIdentity(ctx, account, platform)
}

func (s *AccountService) GetUserInfo(ctx context.Context, uid int64) (*domain.Member, error) {
	return s.Repo.FindByUID(ctx, uid)
}

func (s *AccountService) ListMember(ctx context.Context, page, pageSize int, query, status string) ([]*domain.Member, int64, error) {
	return s.Repo.ListPage(ctx, page, pageSize, query, status)
}

func (s *AccountService) ListIdentities(ctx context.Context, uid int64) ([]*domain.Identity, error) {
	return s.Repo.ListIdentitiesByUID(ctx, uid)
}

func (s *AccountService) FindLoginIDsByUID(ctx context.Context, uid int64) ([]*domain.Identity, error) {
	return s.Repo.ListIdentitiesByUID(ctx, uid)
}

// ---------- update ----------

func (s *AccountService) UpdateUserToken(ctx context.Context, loginID, platform, newPassword string) error {
	hash, err := HashPassword(newPassword, s.BcryptCost)
	if err != nil {
		return err
	}
	if err := s.Repo.UpdateIdentityPassword(ctx, loginID, platform, hash); err != nil {
		return err
	}
	// sync primary password on member if platform password identity
	ident, err := s.Repo.FindIdentity(ctx, loginID, platform)
	if err != nil {
		return err
	}
	m, err := s.Repo.FindByUID(ctx, ident.UID)
	if err != nil {
		return err
	}
	m.PasswordHash = hash
	m.UpdatedAt = domain.NowNano()
	if err := s.Repo.UpdateMember(ctx, m); err != nil {
		return err
	}
	return s.Repo.RevokeAllRefreshByUID(ctx, ident.UID)
}

func (s *AccountService) UpdateUserInfo(ctx context.Context, uid int64, patch *domain.UpdateUserInfoPatch) (*domain.Member, error) {
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return nil, err
	}
	if patch == nil {
		return m, nil
	}
	if patch.DisplayName != nil {
		m.DisplayName = *patch.DisplayName
	}
	if patch.Nickname != nil {
		m.Nickname = *patch.Nickname
	}
	if patch.FullName != nil {
		m.FullName = *patch.FullName
	}
	if patch.AvatarURL != nil {
		m.AvatarURL = *patch.AvatarURL
	}
	if patch.Bio != nil {
		m.Bio = *patch.Bio
	}
	if patch.Timezone != nil {
		m.Timezone = *patch.Timezone
	}
	if patch.NotifyEmail != nil {
		m.NotifyEmail = *patch.NotifyEmail
	}
	if patch.GenderCode != nil {
		m.GenderCode = *patch.GenderCode
	}
	if patch.Birthdate != nil {
		m.Birthdate = *patch.Birthdate
	}
	if patch.National != nil {
		m.National = *patch.National
	}
	if patch.Address != nil {
		m.Address = *patch.Address
	}
	if patch.PostCode != nil {
		m.PostCode = *patch.PostCode
	}
	if patch.PreferredLanguage != nil {
		m.PreferredLanguage = *patch.PreferredLanguage
	}
	if patch.Currency != nil {
		m.Currency = *patch.Currency
	}
	if patch.OnboardingStatus != nil && !domain.IsOnboardingTerminal(m.OnboardingStatus) {
		next := strings.TrimSpace(*patch.OnboardingStatus)
		if next == domain.OnboardingSkipped || next == domain.OnboardingCompleted {
			m.OnboardingStatus = next
			if patch.OnboardingDoneAt != nil && *patch.OnboardingDoneAt > 0 {
				m.OnboardingDoneAt = *patch.OnboardingDoneAt
			} else if m.OnboardingDoneAt == 0 {
				m.OnboardingDoneAt = domain.NowNano()
			}
		}
	}
	if patch.Email != nil {
		newEmail := strings.ToLower(strings.TrimSpace(*patch.Email))
		if newEmail != "" && newEmail != m.Email {
			m.Email = newEmail
			m.EmailVerified = false
			m.EmailVerifiedAt = nil
		}
	}
	if patch.Phone != nil {
		m.Phone = strings.TrimSpace(*patch.Phone)
		m.PhoneVerified = false
		m.PhoneVerifiedAt = nil
	}
	passwordChanged := patch.NewPassword != nil && *patch.NewPassword != ""
	if passwordChanged {
		if patch.CurrentPassword == nil || !CheckPasswordHash(*patch.CurrentPassword, m.PasswordHash) {
			return nil, domain.ErrBadPassword
		}
		hash, err := HashPassword(*patch.NewPassword, s.BcryptCost)
		if err != nil {
			return nil, err
		}
		m.PasswordHash = hash
		// update platform identity password if exists
		if m.Email != "" {
			_ = s.Repo.UpdateIdentityPassword(ctx, m.Email, domain.PlatformPassword, hash)
		}
	}
	m.UpdatedAt = domain.NowNano()
	if err := s.Repo.UpdateMember(ctx, m); err != nil {
		return nil, err
	}
	if passwordChanged {
		if err := s.Repo.RevokeAllRefreshByUID(ctx, uid); err != nil {
			return nil, err
		}
	}
	return m, nil
}

func (s *AccountService) UpdateStatus(ctx context.Context, uid int64, status string) (*domain.Member, error) {
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return nil, err
	}
	m.Status = status
	m.UpdatedAt = domain.NowNano()
	if err := s.Repo.UpdateMember(ctx, m); err != nil {
		return nil, err
	}
	return m, nil
}

// ---------- session ----------

func (s *AccountService) Refresh(ctx context.Context, refreshToken string) (*tokenDomain.TokenPair, error) {
	claims, err := s.Issuer.ParseRefresh(refreshToken)
	if err != nil {
		return nil, err
	}
	uid, ok := s.Repo.ConsumeRefresh(claims.JTI)
	if !ok || uid != claims.UID {
		return nil, tokenDomain.ErrInvalidToken
	}
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return nil, err
	}
	if m.IsSuspended() {
		return nil, domain.ErrSuspended
	}
	return s.issueSession(m)
}

func (s *AccountService) Logout(refreshToken string) {
	if refreshToken == "" {
		return
	}
	if claims, err := s.Issuer.ParseRefresh(refreshToken); err == nil {
		s.Repo.RevokeRefresh(claims.JTI)
	}
}

func (s *AccountService) LogoutAll(ctx context.Context, uid int64) error {
	return s.Repo.RevokeAllRefreshByUID(ctx, uid)
}

// ---------- binding ----------

func (s *AccountService) BindAccount(ctx context.Context, uid int64, loginID, platform, accountType, password string) (*domain.Identity, error) {
	if _, err := s.Repo.FindByUID(ctx, uid); err != nil {
		return nil, err
	}
	platform = strings.ToLower(strings.TrimSpace(platform))
	if platform == "" {
		platform = domain.PlatformPassword
	}
	loginID = strings.TrimSpace(loginID)
	if platform == domain.PlatformPassword {
		loginID = strings.ToLower(loginID)
	}
	if _, err := s.Repo.FindIdentity(ctx, loginID, platform); err == nil {
		return nil, domain.ErrIdentityTaken
	}
	var hash string
	if platform == domain.PlatformPassword && password != "" {
		var err error
		hash, err = HashPassword(password, s.BcryptCost)
		if err != nil {
			return nil, err
		}
	}
	if accountType == "" {
		if platform == domain.PlatformPassword {
			accountType = domain.AccountTypeEmail
		} else {
			accountType = domain.AccountTypePlatform
		}
	}
	now := domain.NowNano()
	ident := &domain.Identity{
		UID: uid, LoginID: loginID, Platform: platform,
		AccountType: accountType, PasswordHash: hash, CreatedAt: now, UpdatedAt: now,
	}
	if err := s.Repo.CreateIdentity(ctx, ident); err != nil {
		return nil, err
	}
	return ident, nil
}

func (s *AccountService) UnbindAccount(ctx context.Context, uid int64, loginID, platform string) error {
	ident, err := s.Repo.FindIdentity(ctx, loginID, platform)
	if err != nil {
		return err
	}
	if ident.UID != uid {
		return domain.ErrIdentityNotFound
	}
	// refuse unbind last identity
	list, err := s.Repo.ListIdentitiesByUID(ctx, uid)
	if err != nil {
		return err
	}
	if len(list) <= 1 {
		return domain.ErrAlreadyBound // reuse: cannot unbind last
	}
	return s.Repo.DeleteIdentity(ctx, loginID, platform)
}

func (s *AccountService) BindVerifyEmail(ctx context.Context, uid int64, email string) error {
	email = strings.ToLower(strings.TrimSpace(email))
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return err
	}
	now := domain.NowNano()
	m.Email = email
	m.EmailVerified = true
	m.EmailVerifiedAt = &now
	if m.Status == domain.StatusUnverified {
		m.Status = domain.StatusActive
	}
	m.UpdatedAt = now
	if err := s.Repo.UpdateMember(ctx, m); err != nil {
		return err
	}
	// ensure identity
	if _, err := s.Repo.FindIdentity(ctx, email, domain.PlatformPassword); err != nil {
		_ = s.Repo.CreateIdentity(ctx, &domain.Identity{
			UID: uid, LoginID: email, Platform: domain.PlatformPassword,
			AccountType: domain.AccountTypeEmail, PasswordHash: m.PasswordHash,
			CreatedAt: now, UpdatedAt: now,
		})
	}
	return nil
}

func (s *AccountService) BindVerifyPhone(ctx context.Context, uid int64, phone string) error {
	phone = strings.TrimSpace(phone)
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return err
	}
	now := domain.NowNano()
	m.Phone = phone
	m.PhoneVerified = true
	m.PhoneVerifiedAt = &now
	m.UpdatedAt = now
	return s.Repo.UpdateMember(ctx, m)
}

// ---------- codes ----------

func (s *AccountService) GenerateCode(ctx context.Context, loginID, codeType string) (string, error) {
	code := randomDigits(6)
	if !s.Repo.SetCode(codeType, strings.ToLower(loginID), code, 10*time.Minute) {
		return "", domain.ErrCodeRateLimited
	}
	return code, nil
}

func (s *AccountService) VerifyCode(ctx context.Context, loginID, codeType, code string, consume bool) error {
	key := strings.ToLower(loginID)
	ok := false
	if consume {
		ok = s.Repo.CheckCode(codeType, key, code)
	} else {
		ok = s.Repo.PeekCode(codeType, key, code)
	}
	if !ok {
		return domain.ErrInvalidCode
	}
	return nil
}

func (s *AccountService) VerifyPlatformAuthResult(ctx context.Context, loginID, platform, password string) (bool, error) {
	ident, err := s.Repo.FindIdentity(ctx, loginID, platform)
	if err != nil {
		return false, err
	}
	if ident.PasswordHash == "" {
		return false, nil
	}
	return CheckPasswordHash(password, ident.PasswordHash), nil
}

// ---------- OAuth providers ----------

func (s *AccountService) VerifyGoogleIDToken(ctx context.Context, idToken string) (subject, email, name string, err error) {
	if strings.TrimSpace(s.GoogleClientID) == "" {
		return "", "", "", domain.ErrOAuthFailed
	}
	info, e := googleIDTokenInfo(ctx, idToken, s.GoogleClientID)
	if e != nil {
		return "", "", "", domain.ErrOAuthFailed
	}
	return info.Subject, info.Email, info.Name, nil
}

func (s *AccountService) LineCodeToProfile(ctx context.Context, code, redirectURI string) (subject, displayName, email string, err error) {
	if s.LineClientID == "" || s.LineClientSecret == "" {
		return "", "", "", domain.ErrOAuthFailed
	}
	if redirectURI == "" {
		redirectURI = s.LineRedirectURI
	}
	tok, e := lineExchangeCode(ctx, code, redirectURI, s.LineClientID, s.LineClientSecret)
	if e != nil {
		return "", "", "", domain.ErrOAuthFailed
	}
	prof, e := lineProfile(ctx, tok)
	if e != nil {
		return "", "", "", domain.ErrOAuthFailed
	}
	return prof.UserID, prof.DisplayName, "", nil
}

// ---------- Harbor Desk convenience ----------

func (s *AccountService) RegisterEmail(ctx context.Context, email, password, displayName string) (*domain.Member, *tokenDomain.TokenPair, error) {
	return s.CreateUserAccount(ctx, domain.CreateLoginUserRequest{
		LoginID: email, Platform: domain.PlatformPassword, AccountType: domain.AccountTypeEmail,
		Password: password, DisplayName: displayName, Email: email,
	})
}

// Register — alias for logic layer
func (s *AccountService) Register(ctx context.Context, email, password, displayName string) (*domain.Member, *tokenDomain.TokenPair, error) {
	return s.RegisterEmail(ctx, email, password, displayName)
}

func (s *AccountService) Me(ctx context.Context, uid int64) (*domain.Member, error) {
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return nil, err
	}
	if m.IsSuspended() {
		return nil, domain.ErrSuspended
	}
	return m, nil
}

func (s *AccountService) RequestPasswordReset(ctx context.Context, email string) (string, error) {
	email = strings.ToLower(strings.TrimSpace(email))
	if email == "" || !strings.Contains(email, "@") {
		return "", domain.ErrEmailNotRegistered
	}
	code := randomDigits(6)
	// try identity or member — unknown email must error (product: 無此信箱不寄)
	if _, err := s.Repo.FindByEmail(ctx, email); err == nil {
		if !s.Repo.SetCode(domain.CodeTypeForgetPassword, email, code, 30*time.Minute) {
			return "", domain.ErrCodeRateLimited
		}
		return code, nil
	}
	if _, err := s.Repo.FindIdentity(ctx, email, domain.PlatformPassword); err == nil {
		if !s.Repo.SetCode(domain.CodeTypeForgetPassword, email, code, 30*time.Minute) {
			return "", domain.ErrCodeRateLimited
		}
		return code, nil
	}
	return "", domain.ErrEmailNotRegistered
}

func (s *AccountService) ResetPassword(ctx context.Context, email, code, newPassword string) error {
	email = strings.ToLower(strings.TrimSpace(email))
	code = strings.TrimSpace(code)
	// 1) 先驗密碼政策（失敗不消費驗證碼）
	if err := ValidatePasswordPolicy(newPassword); err != nil {
		return err
	}
	// 2) Peek：輸錯／未過期都不刪（與信箱驗證同一套）
	match := s.Repo.PeekCode(domain.CodeTypeForgetPassword, email, code) ||
		s.Repo.PeekCode("reset", email, code)
	if !match {
		return domain.ErrInvalidCode
	}
	// 3) 業務先寫入；成功才消費碼
	if err := s.UpdateUserToken(ctx, email, domain.PlatformPassword, newPassword); err != nil {
		if !errors.Is(err, domain.ErrIdentityNotFound) {
			return err
		}
		// seed / legacy member without identity row
		hash, herr := HashPassword(newPassword, s.BcryptCost)
		if herr != nil {
			return herr
		}
		m, err := s.Repo.FindByEmail(ctx, email)
		if err != nil {
			return domain.ErrNotFound
		}
		m.PasswordHash = hash
		m.UpdatedAt = domain.NowNano()
		if err := s.Repo.UpdateMember(ctx, m); err != nil {
			return err
		}
		_ = s.Repo.CreateIdentity(ctx, &domain.Identity{
			UID: m.UID, LoginID: email, Platform: domain.PlatformPassword,
			AccountType: domain.AccountTypeEmail, PasswordHash: hash,
			CreatedAt: domain.NowNano(), UpdatedAt: domain.NowNano(),
		})
		if err := s.Repo.RevokeAllRefreshByUID(ctx, m.UID); err != nil {
			return err
		}
	}
	// 4) 成功才刪碼（兩種 kind 都清）
	_ = s.Repo.CheckCode(domain.CodeTypeForgetPassword, email, code)
	_ = s.Repo.CheckCode("reset", email, code)
	return nil
}

func (s *AccountService) SendEmailCode(ctx context.Context, uid int64) (string, error) {
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return "", err
	}
	code := randomDigits(6)
	key := m.Email
	if key == "" {
		key = fmt.Sprintf("%d", uid)
	}
	if !s.Repo.SetCode(domain.CodeTypeEmail, strings.ToLower(key), code, 30*time.Minute) {
		return "", domain.ErrCodeRateLimited
	}
	// also store by uid for VerifyEmailCode
	if key != fmt.Sprintf("%d", uid) {
		_ = s.Repo.SetCode(domain.CodeTypeEmail, fmt.Sprintf("%d", uid), code, 30*time.Minute)
	}
	return code, nil
}

func (s *AccountService) VerifyEmailCode(ctx context.Context, uid int64, code string) (*domain.Member, error) {
	m, err := s.Repo.FindByUID(ctx, uid)
	if err != nil {
		return nil, err
	}
	uidKey := fmt.Sprintf("%d", uid)
	emailKey := ""
	if m.Email != "" {
		emailKey = strings.ToLower(m.Email)
	}
	code = strings.TrimSpace(code)
	// 與重設密碼同一套：Peek（輸錯不刪）→ 業務成功 → CheckCode 消費
	match := s.Repo.PeekCode(domain.CodeTypeEmail, uidKey, code)
	if !match && emailKey != "" {
		match = s.Repo.PeekCode(domain.CodeTypeEmail, emailKey, code)
	}
	if !match {
		return nil, domain.ErrInvalidCode
	}
	now := domain.NowNano()
	m.EmailVerified = true
	m.EmailVerifiedAt = &now
	if m.Status == domain.StatusUnverified {
		m.Status = domain.StatusActive
	}
	m.UpdatedAt = now
	if err := s.Repo.UpdateMember(ctx, m); err != nil {
		// 業務失敗：碼仍有效，可重試
		return nil, err
	}
	// 成功才消費兩把 key
	_ = s.Repo.CheckCode(domain.CodeTypeEmail, uidKey, code)
	if emailKey != "" {
		_ = s.Repo.CheckCode(domain.CodeTypeEmail, emailKey, code)
	}
	return m, nil
}

// VerifyEmail — alias
func (s *AccountService) VerifyEmail(ctx context.Context, uid int64, code string) (*domain.Member, error) {
	return s.VerifyEmailCode(ctx, uid, code)
}

// ---------- helpers ----------

func (s *AccountService) issueSession(m *domain.Member) (*tokenDomain.TokenPair, error) {
	pair, jti, exp, err := s.Issuer.Issue(m.UID, m.Roles)
	if err != nil {
		return nil, err
	}
	s.Repo.SaveRefresh(jti, m.UID, exp)
	return pair, nil
}

func NewInviteCode() string {
	var b [4]byte
	_, _ = rand.Read(b[:])
	return strings.ToUpper(hex.EncodeToString(b[:]))
}

func randomDigits(n int) string {
	var b strings.Builder
	for i := 0; i < n; i++ {
		v, _ := rand.Int(rand.Reader, big.NewInt(10))
		b.WriteByte(byte('0' + v.Int64()))
	}
	return b.String()
}

func looksLikePhone(s string) bool {
	if len(s) < 8 {
		return false
	}
	for _, c := range s {
		if c >= '0' && c <= '9' || c == '+' {
			continue
		}
		return false
	}
	return true
}
