package service

import (
	"context"
	"errors"
	"strings"

	"github.com/user-management-system/internal/domain"
)

func (s *AuthService) SendEmailBindCode(ctx context.Context, userID int64, email string) error {
	if s == nil || s.userRepo == nil || s.emailCodeSvc == nil {
		return errors.New("email binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}

	normalizedEmail := strings.TrimSpace(email)
	if normalizedEmail == "" {
		return errors.New("email is required")
	}
	if strings.EqualFold(strings.TrimSpace(domain.DerefStr(user.Email)), normalizedEmail) {
		return errors.New("email is already bound to the current account")
	}

	exists, err := s.userRepo.ExistsByEmail(ctx, normalizedEmail)
	if err != nil {
		return err
	}
	if exists {
		return errors.New("email already in use")
	}

	return s.emailCodeSvc.SendEmailCode(ctx, normalizedEmail, "bind")
}

func (s *AuthService) BindEmail(
	ctx context.Context,
	userID int64,
	email string,
	code string,
	currentPassword string,
	totpCode string,
) error {
	if s == nil || s.userRepo == nil || s.emailCodeSvc == nil {
		return errors.New("email binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}

	normalizedEmail := strings.TrimSpace(email)
	if normalizedEmail == "" {
		return errors.New("email is required")
	}
	if strings.EqualFold(strings.TrimSpace(domain.DerefStr(user.Email)), normalizedEmail) {
		return errors.New("email is already bound to the current account")
	}

	exists, err := s.userRepo.ExistsByEmail(ctx, normalizedEmail)
	if err != nil {
		return err
	}
	if exists {
		return errors.New("email already in use")
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return err
	}
	if err := s.emailCodeSvc.VerifyEmailCode(ctx, normalizedEmail, "bind", strings.TrimSpace(code)); err != nil {
		return err
	}

	user.Email = domain.StrPtr(normalizedEmail)
	if err := s.userRepo.Update(ctx, user); err != nil {
		return err
	}

	s.cacheUserInfo(ctx, user)
	s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
		"user_id": user.ID,
		"email":   normalizedEmail,
		"action":  "bind_email",
	})
	return nil
}

func (s *AuthService) UnbindEmail(ctx context.Context, userID int64, currentPassword, totpCode string) error {
	if s == nil || s.userRepo == nil {
		return errors.New("email binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}
	if strings.TrimSpace(domain.DerefStr(user.Email)) == "" {
		return errors.New("email is not bound")
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return err
	}

	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return err
	}
	if s.availableLoginMethodCountAfterContactRemoval(user, accounts, true, false) == 0 {
		return errors.New("at least one login method must remain after unbinding")
	}

	user.Email = nil
	if err := s.userRepo.Update(ctx, user); err != nil {
		return err
	}

	s.cacheUserInfo(ctx, user)
	s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
		"user_id": user.ID,
		"action":  "unbind_email",
	})
	return nil
}

func (s *AuthService) SendPhoneBindCode(ctx context.Context, userID int64, phone string) (*SendCodeResponse, error) {
	if s == nil || s.userRepo == nil || s.smsCodeSvc == nil {
		return nil, errors.New("phone binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if err := s.ensureUserActive(user); err != nil {
		return nil, err
	}

	normalizedPhone := strings.TrimSpace(phone)
	if normalizedPhone == "" {
		return nil, errors.New("phone is required")
	}
	if strings.TrimSpace(domain.DerefStr(user.Phone)) == normalizedPhone {
		return nil, errors.New("phone is already bound to the current account")
	}

	exists, err := s.userRepo.ExistsByPhone(ctx, normalizedPhone)
	if err != nil {
		return nil, err
	}
	if exists {
		return nil, errors.New("phone already in use")
	}

	return s.smsCodeSvc.SendCode(ctx, &SendCodeRequest{
		Phone:   normalizedPhone,
		Purpose: "bind",
	})
}

func (s *AuthService) BindPhone(
	ctx context.Context,
	userID int64,
	phone string,
	code string,
	currentPassword string,
	totpCode string,
) error {
	if s == nil || s.userRepo == nil || s.smsCodeSvc == nil {
		return errors.New("phone binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}

	normalizedPhone := strings.TrimSpace(phone)
	if normalizedPhone == "" {
		return errors.New("phone is required")
	}
	if strings.TrimSpace(domain.DerefStr(user.Phone)) == normalizedPhone {
		return errors.New("phone is already bound to the current account")
	}

	exists, err := s.userRepo.ExistsByPhone(ctx, normalizedPhone)
	if err != nil {
		return err
	}
	if exists {
		return errors.New("phone already in use")
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return err
	}
	if err := s.smsCodeSvc.VerifyCode(ctx, normalizedPhone, "bind", strings.TrimSpace(code)); err != nil {
		return err
	}

	user.Phone = domain.StrPtr(normalizedPhone)
	if err := s.userRepo.Update(ctx, user); err != nil {
		return err
	}

	s.cacheUserInfo(ctx, user)
	s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
		"user_id": user.ID,
		"phone":   normalizedPhone,
		"action":  "bind_phone",
	})
	return nil
}

func (s *AuthService) UnbindPhone(ctx context.Context, userID int64, currentPassword, totpCode string) error {
	if s == nil || s.userRepo == nil {
		return errors.New("phone binding is not configured")
	}

	user, err := s.userRepo.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if err := s.ensureUserActive(user); err != nil {
		return err
	}
	if strings.TrimSpace(domain.DerefStr(user.Phone)) == "" {
		return errors.New("phone is not bound")
	}
	if err := s.verifySensitiveAction(ctx, user, currentPassword, totpCode); err != nil {
		return err
	}

	accounts, err := s.GetSocialAccounts(ctx, userID)
	if err != nil {
		return err
	}
	if s.availableLoginMethodCountAfterContactRemoval(user, accounts, false, true) == 0 {
		return errors.New("at least one login method must remain after unbinding")
	}

	user.Phone = nil
	if err := s.userRepo.Update(ctx, user); err != nil {
		return err
	}

	s.cacheUserInfo(ctx, user)
	s.publishEvent(ctx, domain.EventUserUpdated, map[string]interface{}{
		"user_id": user.ID,
		"action":  "unbind_phone",
	})
	return nil
}

func (s *AuthService) availableLoginMethodCountAfterContactRemoval(
	user *domain.User,
	accounts []*domain.SocialAccount,
	removeEmail bool,
	removePhone bool,
) int {
	if user == nil {
		return 0
	}

	count := 0
	if strings.TrimSpace(user.Password) != "" {
		count++
	}
	if !removeEmail && s.emailCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Email)) != "" {
		count++
	}
	if !removePhone && s.smsCodeSvc != nil && strings.TrimSpace(domain.DerefStr(user.Phone)) != "" {
		count++
	}

	for _, account := range accounts {
		if account == nil || account.Status != domain.SocialAccountStatusActive {
			continue
		}
		count++
	}

	return count
}
