package auth

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

	jwt "github.com/golang-jwt/jwt/v5"
	"hyapp/pkg/appcode"
)

// ErrAccessTokenExpired 让 transport 精确区分“可通过 refresh 恢复”的 JWT 过期与签名非法/缺失凭证。
var ErrAccessTokenExpired = errors.New("access token expired")

const gameProviderTokenType = "game_provider"

type contextKey string

const (
	userContextKey   contextKey = "gateway_user_id"
	claimsContextKey contextKey = "gateway_claims"
)

// Claims 是 gateway 从 access token 中消费的最小用户状态快照。
// 它只用于入口鉴权和 profile gate，不替代 user-service 的用户主数据。
type Claims struct {
	AppCode   string
	UserID    int64
	SessionID string
	// DeviceID 是 user-service 从 auth_session 签入 access token 的设备绑定；旧 token 缺字段时保持为空。
	DeviceID               string
	ProfileCompleted       bool
	OnboardingStatus       string
	AccessTokenExpiresAtMS int64
}

// Verifier 负责在 gateway 入口层校验用户 JWT。
type Verifier struct {
	secret []byte
}

// NewVerifier 初始化入口 JWT 校验器。
func NewVerifier(secret string) *Verifier {
	return &Verifier{secret: []byte(secret)}
}

// VerifyUserID 只负责从 Authorization header 中提取并校验用户身份。
// HTTP 状态码和响应 envelope 属于 transport 层，auth 包不直接写 ResponseWriter。
func (v *Verifier) VerifyUserID(header string) (int64, error) {
	claims, err := v.Verify(header)
	if err != nil {
		return 0, err
	}

	return claims.UserID, nil
}

// Verify 校验 access token 并返回 gateway 入口需要的用户状态快照。
func (v *Verifier) Verify(header string) (Claims, error) {
	return v.verify(header, false)
}

// VerifyGameCallbackToken 同时接受历史 access token 与新的短期游戏令牌。
// game_provider 令牌只能走厂商回调入口；普通 API 仍由 Verify 拒绝，避免短令牌被当作登录凭证复用。
func (v *Verifier) VerifyGameCallbackToken(header string) (Claims, error) {
	return v.verify(header, true)
}

func (v *Verifier) verify(header string, allowGameProvider bool) (Claims, error) {
	if !strings.HasPrefix(header, "Bearer ") {
		return Claims{}, fmt.Errorf("missing bearer token")
	}

	tokenString := strings.TrimPrefix(header, "Bearer ")
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
		if token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
			return nil, fmt.Errorf("unexpected signing method")
		}

		return v.secret, nil
	})
	if errors.Is(err, jwt.ErrTokenExpired) && !errors.Is(err, jwt.ErrTokenNotValidYet) && !errors.Is(err, jwt.ErrInvalidType) {
		// jwt 先验签再校验 claims，因此到这里签名已可信；但 exp 过期若同时伴随未来 nbf/畸形时间字段，
		// 仍是非法凭证而非单纯可 refresh 的过期 token，不能向客户端发刷新信号。
		return Claims{}, ErrAccessTokenExpired
	}
	if err != nil || !token.Valid {
		return Claims{}, fmt.Errorf("invalid token")
	}

	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return Claims{}, fmt.Errorf("invalid claims")
	}
	tokenType, _ := claims["typ"].(string)
	tokenType = strings.TrimSpace(tokenType)
	if tokenType != "" && tokenType != "access" && !(allowGameProvider && tokenType == gameProviderTokenType) {
		return Claims{}, fmt.Errorf("invalid token type")
	}

	rawUserID, ok := claims["user_id"].(string)
	if !ok || strings.TrimSpace(rawUserID) == "" {
		return Claims{}, fmt.Errorf("missing user_id")
	}
	userID, err := strconv.ParseInt(strings.TrimSpace(rawUserID), 10, 64)
	if err != nil || userID <= 0 {
		return Claims{}, fmt.Errorf("invalid user_id")
	}

	profileCompleted, _ := claims["profile_completed"].(bool)
	onboardingStatus, _ := claims["onboarding_status"].(string)
	sessionID, _ := claims["sid"].(string)
	deviceID, _ := claims["device_id"].(string)
	appCode, _ := claims["app_code"].(string)
	expiresAtMS := claimUnixSecondsMS(claims["exp"])

	return Claims{
		AppCode:                appcode.Normalize(appCode),
		UserID:                 userID,
		SessionID:              strings.TrimSpace(sessionID),
		DeviceID:               strings.TrimSpace(deviceID),
		ProfileCompleted:       profileCompleted,
		OnboardingStatus:       strings.TrimSpace(onboardingStatus),
		AccessTokenExpiresAtMS: expiresAtMS,
	}, nil
}

// IssueGameProviderToken 把已完成入口鉴权的用户身份压缩成厂商专用 JWT。
// 令牌只包含回调归因需要的 App、用户、过期时间和随机 ID，避免把不断增长的完整登录 claims 暴露给厂商。
func (v *Verifier) IssueGameProviderToken(appCodeValue string, userID int64, ttl time.Duration) (string, error) {
	if v == nil || len(v.secret) == 0 || userID <= 0 || ttl <= 0 {
		return "", fmt.Errorf("invalid game provider token input")
	}
	var nonce [12]byte
	if _, err := rand.Read(nonce[:]); err != nil {
		return "", fmt.Errorf("generate game provider token id: %w", err)
	}
	now := time.Now().UTC()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"app_code": appcode.Normalize(appCodeValue),
		"exp":      now.Add(ttl).Unix(),
		"iat":      now.Unix(),
		"jti":      hex.EncodeToString(nonce[:]),
		"typ":      gameProviderTokenType,
		"user_id":  strconv.FormatInt(userID, 10),
	})
	signed, err := token.SignedString(v.secret)
	if err != nil {
		return "", fmt.Errorf("sign game provider token: %w", err)
	}
	return signed, nil
}

func claimUnixSecondsMS(value any) int64 {
	switch typed := value.(type) {
	case float64:
		if typed <= 0 {
			return 0
		}
		return int64(typed) * 1000
	case int64:
		if typed <= 0 {
			return 0
		}
		return typed * 1000
	case int:
		if typed <= 0 {
			return 0
		}
		return int64(typed) * 1000
	case string:
		parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
		if err != nil || parsed <= 0 {
			return 0
		}
		return parsed * 1000
	default:
		return 0
	}
}

// WithUserID 把已鉴权用户写入请求上下文，供 gateway 组装 RequestMeta。
func WithUserID(ctx context.Context, userID int64) context.Context {
	return context.WithValue(ctx, userContextKey, userID)
}

// WithClaims 把 access token 中的准入快照写入上下文。
func WithClaims(ctx context.Context, claims Claims) context.Context {
	ctx = appcode.WithContext(ctx, claims.AppCode)
	ctx = WithUserID(ctx, claims.UserID)
	return context.WithValue(ctx, claimsContextKey, claims)
}

// UserIDFromContext 提取鉴权后的用户 ID。
func UserIDFromContext(ctx context.Context) int64 {
	userID, _ := ctx.Value(userContextKey).(int64)

	return userID
}

// SessionIDFromContext 提取 access token 中的服务端 session_id。
func SessionIDFromContext(ctx context.Context) string {
	claims, _ := ctx.Value(claimsContextKey).(Claims)
	return strings.TrimSpace(claims.SessionID)
}

// DeviceIDFromContext 只返回已验签 access token 的设备 claim。
// 不从请求体、header 或 session_id 兜底，否则 dynamic_v3 设备日风控可被轻易分裂。
func DeviceIDFromContext(ctx context.Context) string {
	claims, _ := ctx.Value(claimsContextKey).(Claims)
	return strings.TrimSpace(claims.DeviceID)
}

// ProfileCompletedFromContext 返回 gateway profile gate 的判断依据。
func ProfileCompletedFromContext(ctx context.Context) bool {
	claims, _ := ctx.Value(claimsContextKey).(Claims)
	return claims.ProfileCompleted
}

// OnboardingStatusFromContext 暴露 token 中的注册状态，便于后续审计或灰度判断。
func OnboardingStatusFromContext(ctx context.Context) string {
	claims, _ := ctx.Value(claimsContextKey).(Claims)
	return claims.OnboardingStatus
}

// AccessTokenExpiresAtMSFromContext 返回当前 access token 的过期时间；旧测试 token 没有 exp 时返回 0。
func AccessTokenExpiresAtMSFromContext(ctx context.Context) int64 {
	claims, _ := ctx.Value(claimsContextKey).(Claims)
	return claims.AccessTokenExpiresAtMS
}
