package auth

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"
	"time"

	jwt "github.com/golang-jwt/jwt/v5"
	"hyapp/pkg/appcode"
	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	authdomain "hyapp/services/user-service/internal/domain/auth"
	userdomain "hyapp/services/user-service/internal/domain/user"
)

// RefreshToken 轮换 refresh token，旧 token 成功使用后立即失效。
func (s *Service) RefreshToken(ctx context.Context, refreshToken string, deviceID string, meta Meta) (authdomain.Token, error) {
	if strings.TrimSpace(refreshToken) == "" || strings.TrimSpace(deviceID) == "" {
		// refresh token 和 device_id 共同定位并校验会话归属。
		return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "refresh_token and device_id are required")
	}
	if s.authRepository == nil {
		return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	nowMs := s.now().UnixMilli()
	// 持久层只保存 refresh token hash，不能用原文查询。
	session, err := s.authRepository.FindActiveSessionByRefreshHash(ctx, hashRefreshToken(refreshToken), nowMs)
	if err != nil {
		reason := xerr.SessionRevoked
		if xerr.IsCode(err, xerr.SessionExpired) {
			reason = xerr.SessionExpired
		}
		s.audit(ctx, meta, 0, loginRefresh, "", resultFailed, string(reason))
		return authdomain.Token{}, err
	}
	if subtle.ConstantTimeCompare([]byte(session.DeviceID), []byte(deviceID)) != 1 {
		// refresh token 绑定设备，跨设备使用按 revoked 处理。
		s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.SessionRevoked))
		return authdomain.Token{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if session.AppCode != appcode.Normalize(meta.AppCode) {
		// refresh token 不能跨 App 使用；即使 refresh hash 泄漏也不能从另一个包名续期。
		s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.SessionRevoked))
		return authdomain.Token{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	// refresh 时重新读取用户，确保禁用状态和靓号过期恢复生效。
	user, err := s.freshUser(ctx, session.UserID, nowMs, meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}
	if !user.CanLogin() {
		// 禁用用户不能续期会话。
		s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.UserDisabled))
		return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled")
	}

	// refresh token 成功使用后必须轮换，旧 session 立即吊销。
	newSession, newRefreshToken, err := s.newSession(session.AppCode, session.UserID, deviceID)
	if err != nil {
		return authdomain.Token{}, err
	}
	if err := s.authRepository.ReplaceSession(ctx, session.SessionID, newSession, nowMs); err != nil {
		s.audit(ctx, meta, session.UserID, loginRefresh, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}

	s.audit(ctx, meta, session.UserID, loginRefresh, "", resultSuccess, "")
	return s.issueToken(user, newSession.SessionID, newRefreshToken)
}

// IssueAccessTokenForSession 基于当前 active session 重签 access token，不轮换 refresh token。
// 该链路用于 CompleteOnboarding 后刷新 profile_completed 快照，避免客户端必须额外 refresh。
func (s *Service) IssueAccessTokenForSession(ctx context.Context, userID int64, sessionID string, meta Meta) (authdomain.Token, error) {
	sessionID = strings.TrimSpace(sessionID)
	if userID <= 0 || sessionID == "" {
		// session_id 来自已校验 JWT 的 sid claim，缺失说明调用方不能安全重签。
		return authdomain.Token{}, xerr.New(xerr.InvalidArgument, "user_id and session_id are required")
	}
	if s.authRepository == nil {
		return authdomain.Token{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	nowMs := s.now().UnixMilli()
	session, err := s.authRepository.FindActiveSessionByID(ctx, sessionID, nowMs)
	if err != nil {
		s.audit(ctx, meta, userID, loginAccessResign, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}
	if session.UserID != userID {
		// sid 和 user_id 必须同源于同一个 access token，任何不一致都按会话失效处理。
		s.audit(ctx, meta, userID, loginAccessResign, "", resultFailed, string(xerr.SessionRevoked))
		return authdomain.Token{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if session.AppCode != appcode.Normalize(meta.AppCode) {
		s.audit(ctx, meta, userID, loginAccessResign, "", resultFailed, string(xerr.SessionRevoked))
		return authdomain.Token{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}

	user, err := s.freshUser(ctx, userID, nowMs, meta.RequestID)
	if err != nil {
		s.audit(ctx, meta, userID, loginAccessResign, "", resultFailed, string(xerr.CodeOf(err)))
		return authdomain.Token{}, err
	}
	if !user.CanLogin() {
		s.audit(ctx, meta, userID, loginAccessResign, "", resultFailed, string(xerr.UserDisabled))
		return authdomain.Token{}, xerr.New(xerr.UserDisabled, "user is disabled")
	}

	s.audit(ctx, meta, userID, loginAccessResign, "", resultSuccess, "")
	return s.issueToken(user, session.SessionID, "")
}

// Logout 失效指定 session 或 refresh token。
func (s *Service) Logout(ctx context.Context, sessionID string, refreshToken string, meta Meta) (bool, error) {
	if strings.TrimSpace(sessionID) == "" && strings.TrimSpace(refreshToken) == "" {
		// 至少需要一种 session 定位方式。
		return false, xerr.New(xerr.InvalidArgument, "session_id or refresh_token is required")
	}
	if s.authRepository == nil {
		return false, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	// refreshToken 可能为空；hashRefreshToken 会保持空值，支持仅用 session_id 登出。
	revoked, err := s.authRepository.RevokeSession(ctx, sessionID, hashRefreshToken(refreshToken), s.now().UnixMilli())
	if err != nil {
		s.audit(ctx, meta, 0, "logout", "", resultFailed, string(xerr.CodeOf(err)))
		return false, err
	}

	s.audit(ctx, meta, 0, "logout", "", resultSuccess, "")
	return revoked, nil
}

// AppHeartbeat 刷新当前 App 登录会话的服务端在线时间。
func (s *Service) AppHeartbeat(ctx context.Context, userID int64, sessionID string, meta Meta) (authdomain.Session, error) {
	sessionID = strings.TrimSpace(sessionID)
	if userID <= 0 || sessionID == "" {
		// APP 心跳只能续当前 access token 中的会话，不能让客户端传任意 user_id 或空 sid。
		return authdomain.Session{}, xerr.New(xerr.InvalidArgument, "user_id and session_id are required")
	}
	if s.authRepository == nil {
		return authdomain.Session{}, xerr.New(xerr.Unavailable, "auth repository is not configured")
	}

	nowMs := s.now().UnixMilli()
	session, err := s.authRepository.UpdateSessionHeartbeat(ctx, sessionID, userID, nowMs, meta.RequestID)
	if err != nil {
		return authdomain.Session{}, err
	}
	if session.AppCode != appcode.Normalize(meta.AppCode) {
		// 理论上 repository 已按 app_code 限定；这里保留防御，避免错误 context 造成跨 App 心跳成功。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}

	return session, nil
}

// TokenConfigValid 判断签发配置是否具备启动条件。
func (s *Service) TokenConfigValid() bool {
	// 当前只支持 HS256，避免签发和下游解析在算法上分叉。
	return s.cfg.Issuer != "" && s.cfg.AccessTokenTTLSec > 0 && s.cfg.RefreshTokenTTLSec > 0 && s.cfg.SigningAlg == jwt.SigningMethodHS256.Alg() && s.cfg.SigningSecret != ""
}

func (s *Service) newSession(appCode string, userID int64, deviceID string) (authdomain.Session, string, error) {
	// refresh token 原文只返回给调用方，session 中只保存 hash。
	refreshToken, err := randomToken("refresh")
	if err != nil {
		return authdomain.Session{}, "", err
	}

	nowMs := s.now().UnixMilli()
	// SessionID 使用独立前缀，便于日志和排障识别。
	sessionID := idgen.New("sess")
	return authdomain.Session{
		AppCode:          appcode.Normalize(appCode),
		SessionID:        sessionID,
		UserID:           userID,
		RefreshTokenHash: hashRefreshToken(refreshToken),
		DeviceID:         strings.TrimSpace(deviceID),
		ExpiresAtMs:      nowMs + s.cfg.RefreshTokenTTLSec*1000,
		CreatedAtMs:      nowMs,
		UpdatedAtMs:      nowMs,
	}, refreshToken, nil
}

func (s *Service) issueToken(user userdomain.User, sessionID string, refreshToken string) (authdomain.Token, error) {
	now := s.now()
	expiresAt := now.Add(time.Duration(s.cfg.AccessTokenTTLSec) * time.Second)
	onboardingStatus := tokenOnboardingStatus(user)
	// JWT 只携带内部 user_id；客户端展示和人工输入统一使用 display_user_id。
	// user_id 必须写成字符串：雪花 ID 超过 JS/JSON number 的安全整数范围，写成 number 会在 gateway 解析时丢精度。
	claims := jwt.MapClaims{
		"iss":                           s.cfg.Issuer,
		"app_code":                      appcode.Normalize(user.AppCode),
		"sub":                           strconv.FormatInt(user.UserID, 10),
		"user_id":                       strconv.FormatInt(user.UserID, 10),
		"display_user_id":               user.CurrentDisplayUserID,
		"default_display_user_id":       user.DefaultDisplayUserID,
		"display_user_id_kind":          string(user.CurrentDisplayUserIDKind),
		"display_user_id_expires_at_ms": user.CurrentDisplayUserIDExpiresAtMs,
		"profile_completed":             user.ProfileCompleted,
		"onboarding_status":             onboardingStatus,
		"sid":                           sessionID,
		"typ":                           "access",
		"iat":                           now.Unix(),
		"exp":                           expiresAt.Unix(),
	}

	// 当前固定 HS256，配置有效性由 TokenConfigValid 校验。
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	accessToken, err := token.SignedString([]byte(s.cfg.SigningSecret))
	if err != nil {
		return authdomain.Token{}, err
	}

	return authdomain.Token{
		AppCode:                  appcode.Normalize(user.AppCode),
		UserID:                   user.UserID,
		DisplayUserID:            user.CurrentDisplayUserID,
		DefaultDisplayUserID:     user.DefaultDisplayUserID,
		DisplayUserIDKind:        string(user.CurrentDisplayUserIDKind),
		DisplayUserIDExpiresAtMs: user.CurrentDisplayUserIDExpiresAtMs,
		ProfileCompleted:         user.ProfileCompleted,
		OnboardingStatus:         onboardingStatus,
		SessionID:                sessionID,
		AccessToken:              accessToken,
		RefreshToken:             refreshToken,
		ExpiresInSec:             s.cfg.AccessTokenTTLSec,
		TokenType:                tokenType,
	}, nil
}

func tokenOnboardingStatus(user userdomain.User) string {
	// 旧测试数据或手工种子可能没有显式状态；token 按 profile_completed 推导安全默认值。
	if user.OnboardingStatus != "" {
		return string(user.OnboardingStatus)
	}
	if user.ProfileCompleted {
		return string(userdomain.OnboardingStatusCompleted)
	}

	return string(userdomain.OnboardingStatusProfileRequired)
}

func (s *Service) freshUser(ctx context.Context, userID int64, nowMs int64, requestID string) (userdomain.User, error) {
	if s.userRepository == nil {
		// 没有用户读取能力时不能签发 token。
		return userdomain.User{}, xerr.New(xerr.Unavailable, "user repository is not configured")
	}

	user, err := s.userRepository.GetUser(ctx, userID)
	if err != nil {
		return userdomain.User{}, err
	}
	if !user.DisplayUserIDExpired(nowMs) {
		// 当前展示号仍有效，直接返回规范化用户。
		return user, nil
	}

	if s.identityRepository == nil {
		// 发现过期靓号但无法恢复时不能签发带过期短号的 token。
		return userdomain.User{}, xerr.New(xerr.Unavailable, "identity repository is not configured")
	}

	// 登录和 refresh 路径都复用同一过期恢复事务。
	if _, err := s.identityRepository.ExpirePrettyDisplayUserID(ctx, userdomain.ExpirePrettyDisplayUserIDCommand{
		UserID:    userID,
		RequestID: requestID,
		NowMs:     nowMs,
	}); err != nil {
		return userdomain.User{}, err
	}

	// 恢复后重新读取 users，确保 token 使用最新默认短号。
	return s.userRepository.GetUser(ctx, userID)
}

func randomToken(prefix string) (string, error) {
	// 32 字节随机熵足够作为 refresh token 原文，使用 URL-safe base64 返回。
	var entropy [32]byte
	if _, err := rand.Read(entropy[:]); err != nil {
		return "", err
	}

	return fmt.Sprintf("%s_%s", prefix, base64.RawURLEncoding.EncodeToString(entropy[:])), nil
}

func hashRefreshToken(refreshToken string) string {
	if refreshToken == "" {
		// 空 refresh token hash 保持空字符串，支持只按 session_id 吊销。
		return ""
	}

	// refresh token 只持久化 SHA-256 hash，避免数据库泄漏后可直接使用原文。
	sum := sha256.Sum256([]byte(refreshToken))
	return hex.EncodeToString(sum[:])
}
