// Package auth JWT 签发/校验 + jti 黑名单。微信 OAuth 在 wechat.go（5B/5C）。
package auth

import (
	"context"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"

	"dudu/server/internal/store"
	"dudu/server/pkg/protocol"
)

type JWT struct {
	secret []byte
	ttl    time.Duration
	rdb    *redis.Client
}

func NewJWT(secret string, ttl time.Duration, rdb *redis.Client) *JWT {
	return &JWT{secret: []byte(secret), ttl: ttl, rdb: rdb}
}

type Claims struct {
	jwt.RegisteredClaims
}

func (j *JWT) Sign(userID string) (string, error) {
	now := time.Now()
	claims := Claims{RegisteredClaims: jwt.RegisteredClaims{
		Subject:   userID,
		ID:        uuid.NewString(),
		IssuedAt:  jwt.NewNumericDate(now),
		ExpiresAt: jwt.NewNumericDate(now.Add(j.ttl)),
	}}
	return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(j.secret)
}

// Parse 校验签名、过期与黑名单，返回 userID、jti 与 token 自然过期时间。
// 黑名单检查 fail-closed：Redis 不可用（Exists 返回 error）时拒绝放行，
// 宁可误拒也不能让已登出/被踢出的 token 在 Redis 故障窗口复活（17A）。
func (j *JWT) Parse(ctx context.Context, token string) (userID, jti string, expiresAt time.Time, err error) {
	var claims Claims
	_, err = jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (any, error) {
		return j.secret, nil
	}, jwt.WithValidMethods([]string{"HS256"}))
	if err != nil {
		return "", "", time.Time{}, err
	}
	if j.rdb != nil {
		n, exErr := j.rdb.Exists(ctx, store.KeyJwtBlock(claims.ID)).Result()
		if exErr != nil {
			// fail-closed：无法确认黑名单状态时拒绝，避免故障期放行被撤销的 token。
			slog.Warn("jwt blacklist check failed, rejecting (fail-closed)", "jti", claims.ID, "err", exErr)
			return "", "", time.Time{}, exErr
		}
		if n == 1 {
			return "", "", time.Time{}, jwt.ErrTokenExpired
		}
	}
	if claims.ExpiresAt != nil {
		expiresAt = claims.ExpiresAt.Time
	}
	return claims.Subject, claims.ID, expiresAt, nil
}

// Revoke 将 jti 拉黑至 token 自然过期（logout / 踢出）。
func (j *JWT) Revoke(ctx context.Context, jti string, expiresAt time.Time) error {
	ttl := time.Until(expiresAt)
	if ttl <= 0 {
		return nil
	}
	return j.rdb.Set(ctx, store.KeyJwtBlock(jti), 1, ttl).Err()
}

const (
	CtxUserID  = "auth.user_id"
	CtxJTI     = "auth.jti"
	CtxExpires = "auth.expires_at" // token 自然过期时间（time.Time），供 Logout 拉黑到期用
)

// Middleware gin 鉴权中间件：Authorization: Bearer <JWT>。
func (j *JWT) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
		if token == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized))
			return
		}
		uid, jti, exp, err := j.Parse(c.Request.Context(), token)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, protocol.NewAPIError(protocol.ErrUnauthorized))
			return
		}
		c.Set(CtxUserID, uid)
		c.Set(CtxJTI, jti)
		c.Set(CtxExpires, exp)
		c.Next()
	}
}

// OptionalMiddleware 可选鉴权（17F）：带合法 Authorization 时注入 user_id；
// 无 token 或解析失败（含 Redis 故障导致的 fail-closed）一律放行匿名，不拦截。
// 用于 /v1/metrics/batch 等既接受匿名打点、又希望登录用户能关联 user_id 的端点。
func (j *JWT) OptionalMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
		if token != "" {
			if uid, jti, exp, err := j.Parse(c.Request.Context(), token); err == nil {
				c.Set(CtxUserID, uid)
				c.Set(CtxJTI, jti)
				c.Set(CtxExpires, exp)
			}
		}
		c.Next()
	}
}

// UserID 从 gin 上下文取当前用户。
func UserID(c *gin.Context) string { return c.GetString(CtxUserID) }
