package auth

import (
	"context"
	"strings"

	"golang.org/x/crypto/bcrypt"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	authdomain "hyapp/services/user-service/internal/domain/auth"
	userdomain "hyapp/services/user-service/internal/domain/user"
)

// LoginPassword 使用当前 display_user_id 和用户后续设置的密码创建新 session。
// 用户不存在、未设置密码和密码错误统一返回 AUTH_FAILED，避免暴露可枚举身份状态。
func (s *Service) LoginPassword(ctx context.Context, displayUserID string, password string, deviceID string, meta Meta) (authdomain.Token, error) {
	meta.AppCode = appcode.Normalize(meta.AppCode)
	displayUserID = strings.TrimSpace(displayUserID)
	if displayUserID == "" || password == "" || strings.TrimSpace(deviceID) == "" {
		// device_id 是 refresh token 后续轮换绑定条件，登录时必须提供。
		return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "display_user_id, password and device_id are required")
	}
	if s.authRepository == nil {
		// 没有认证 repository 时不能读取密码身份或创建 session。
		return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}
	if !userdomain.ValidResolvableDisplayUserID(displayUserID) {
		// 密码登录接受当前 active 展示号，也接受被 active 靓号覆盖的 held 默认短号；非法字符和不存在账号统一映射 AUTH_FAILED，避免账号枚举。
		s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed))
		return authdomain.Token{}, authFailed()
	}

	nowMs := s.now().UnixMilli()
	// repository 会按输入展示号定位用户，再返回该用户当前 active 展示号；held 默认短号只作为登录别名，不会覆盖 token 展示号。
	accountRecord, err := s.authRepository.FindPasswordByDisplayUserID(ctx, displayUserID, nowMs)
	if err != nil {
		s.audit(ctx, meta, 0, loginPassword, "", resultFailed, string(xerr.AuthFailed))
		return authdomain.Token{}, authFailed()
	}
	if err := verifyPassword(accountRecord.PasswordHash, password); err != nil {
		// 密码错误不暴露为独立 reason，和未设置密码保持同一认证失败语义。
		s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.AuthFailed))
		return authdomain.Token{}, authFailed()
	}

	// 重新读取用户快照，保证禁用状态和过期靓号恢复都在签发前生效。
	user, err := s.freshUser(ctx, accountRecord.UserID, nowMs, meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, accountRecord.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}
	if !user.CanLogin() {
		// disabled/banned 用户不能通过已有密码继续拿新 token。
		s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.UserDisabled))
		return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled")
	}

	// 每次密码登录都会创建新的 refresh session。
	session, refreshToken, err := s.newSession(meta.AppCode, user.UserID, deviceID)
	if err != nil {
		return authdomain.Token{}, err
	}
	if err := s.authRepository.CreateSession(ctx, session); err != nil {
		// session 写入失败时不能返回 token，避免客户端持有没有服务端状态的 refresh token。
		s.audit(ctx, meta, user.UserID, loginPassword, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}

	s.audit(ctx, meta, user.UserID, loginPassword, "", resultSuccess, "")
	// 登录成功即刷新设备档案；写入是 best-effort，不影响 token 返回。
	s.recordUserDevice(ctx, meta, user.UserID, deviceID)
	token, err := s.issueToken(user, session.SessionID, session.DeviceID, refreshToken, session.ExpiresAtMs)
	if err != nil {
		return authdomain.Token{}, err
	}
	s.enqueueLoginIPRiskJob(ctx, meta, token, "account", meta.Platform, meta.Language, meta.Timezone)
	return token, nil
}

// SetPassword 为已经三方登录创建出的用户首次设置密码。
// 该入口必须由 gateway 先校验 access token 后传入 user_id，不能再接受客户端自报用户。
func (s *Service) SetPassword(ctx context.Context, userID int64, password string, meta Meta) error {
	if userID <= 0 || password == "" {
		// user_id 必须来自 gateway 的 access token 校验结果，不能接受客户端自报身份。
		return xerr.New(xerr.InvalidArgument, "user_id and password are required")
	}
	if s.authRepository == nil {
		return xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	user, err := s.freshUser(ctx, userID, s.now().UnixMilli(), meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err)))
		return err
	}
	if !user.CanLogin() {
		// 禁用用户不能新增密码身份。
		s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.UserDisabled))
		return xerr.New(xerr.UserDisabled, "user is disabled")
	}

	// 密码进入持久层前必须先 hash。
	passwordHash, err := hashPassword(password)
	if err != nil {
		return err
	}

	nowMs := s.now().UnixMilli()
	// repository 负责保证一个用户只能首次设置一次密码。
	err = s.authRepository.SetPassword(ctx, authdomain.PasswordAccount{
		AppCode:       appcode.Normalize(meta.AppCode),
		UserID:        user.UserID,
		DisplayUserID: user.CurrentDisplayUserID,
		PasswordHash:  passwordHash,
		HashAlg:       hashAlgBcrypt,
		CreatedAtMs:   nowMs,
		UpdatedAtMs:   nowMs,
	})
	if err != nil {
		s.audit(ctx, meta, userID, loginSetPass, "", resultFailed, string(xerr.CodeOf(err)))
		return err
	}

	s.audit(ctx, meta, userID, loginSetPass, "", resultSuccess, "")
	return nil
}

func hashPassword(password string) (string, error) {
	// bcrypt 默认 cost 作为 v1 密码 hash 策略，后续可通过 HashAlg 升级。
	body, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func verifyPassword(passwordHash string, password string) error {
	// bcrypt 比对失败统一映射 AUTH_FAILED。
	if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
		return authFailed()
	}

	return nil
}
