package auth

import (
	"encoding/json"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"

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

// Handlers 认证路由：扫码（5B）、微信 OAuth（5C）、邮箱验证码、logout。
type Handlers struct {
	DB     *gorm.DB
	RDB    *redis.Client
	JWT    *JWT
	Wechat WechatClient
	Mailer Mailer
	// DevCode 非空时邮箱验证码固定为该值（AUTH_EMAIL_DEV_CODE，仅开发脚本设置）。
	DevCode string
	// QrAuthURL 二维码内容模板（真实环境为微信开放平台授权页，%s 为 state）
	QrAuthURL string
}

const qrTTL = 2 * time.Minute

type qrState struct {
	Status string `json:"status"` // pending | confirmed
	Token  string `json:"token,omitempty"`
	UserID string `json:"user_id,omitempty"`
	Nick   string `json:"nick,omitempty"`
}

// CreateQr POST /v1/auth/qr 🔓
func (h *Handlers) CreateQr(c *gin.Context) {
	state := uuid.NewString()
	b, _ := json.Marshal(qrState{Status: "pending"})
	if err := h.RDB.Set(c, store.KeyAuthQr(state), b, qrTTL).Err(); err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	url := h.QrAuthURL
	if url == "" {
		url = "https://dudu.app/auth/qr/" // mock 占位，接入开放平台后替换
	}
	c.JSON(http.StatusOK, protocol.AuthQrResponse{State: state, QrURL: url + state})
}

// PollQr GET /v1/auth/qr/:state 🔓（桌面端 1s 轮询）
func (h *Handlers) PollQr(c *gin.Context) {
	b, err := h.RDB.Get(c, store.KeyAuthQr(c.Param("state"))).Bytes()
	if err == redis.Nil {
		c.JSON(http.StatusOK, protocol.AuthQrStatusResponse{Status: "expired"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	var s qrState
	_ = json.Unmarshal(b, &s)
	resp := protocol.AuthQrStatusResponse{Status: s.Status}
	if s.Status == "confirmed" {
		resp.Token = s.Token
		resp.User = &protocol.UserInfo{UserID: s.UserID, NicknameMasked: s.Nick}
		// 一次性：下发后即删，防复用
		h.RDB.Del(c, store.KeyAuthQr(c.Param("state")))
	}
	c.JSON(http.StatusOK, resp)
}

// QrCallback GET /v1/auth/wechat/callback?code=&state= 🔓
// 手机微信内授权后回调：code 换身份 → 建号 → 标记 state confirmed。
func (h *Handlers) QrCallback(c *gin.Context) {
	code, state := c.Query("code"), c.Query("state")
	key := store.KeyAuthQr(state)
	if n, _ := h.RDB.Exists(c, key).Result(); n == 0 {
		c.String(http.StatusBadRequest, "二维码已过期，请回到 dudu 重新获取")
		return
	}
	info, err := h.Wechat.ExchangeCode(c, code, "web")
	if err != nil {
		c.String(http.StatusBadRequest, "微信授权失败，请重试")
		return
	}
	user, err := FindOrCreateUser(h.DB, info, "web")
	if err != nil {
		c.String(http.StatusInternalServerError, "服务繁忙，请重试")
		return
	}
	token, err := h.JWT.Sign(user.ID)
	if err != nil {
		c.String(http.StatusInternalServerError, "服务繁忙，请重试")
		return
	}
	b, _ := json.Marshal(qrState{
		Status: "confirmed", Token: token, UserID: user.ID, Nick: MaskNickname(user.Nickname),
	})
	_ = h.RDB.Set(c, key, b, qrTTL).Err()
	c.String(http.StatusOK, "登录成功，回到 dudu 继续")
}

// MobileLogin POST /v1/auth/wechat 🔓（移动端 OpenSDK code）
func (h *Handlers) MobileLogin(c *gin.Context) {
	var req protocol.AuthWechatRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	info, err := h.Wechat.ExchangeCode(c, req.Code, "mobile")
	if err != nil {
		c.JSON(http.StatusBadRequest, protocol.NewAPIError(protocol.ErrBadRequest))
		return
	}
	user, err := FindOrCreateUser(h.DB, info, "mobile")
	if err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	token, err := h.JWT.Sign(user.ID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, protocol.NewAPIError(protocol.ErrInternal))
		return
	}
	c.JSON(http.StatusOK, protocol.AuthTokenResponse{
		Token: token,
		User:  protocol.UserInfo{UserID: user.ID, NicknameMasked: MaskNickname(user.Nickname)},
	})
}

// Logout POST /v1/auth/logout（需登录）
// 拉黑到 token 真实自然过期时间（中间件解析后存于 CtxExpires），确保即便
// JWT_TTL_HOURS 配得很大，注销 token 也不会在固定窗口后复活（17B）。
func (h *Handlers) Logout(c *gin.Context) {
	jti := c.GetString(CtxJTI)
	exp, _ := c.Get(CtxExpires)
	expiresAt, _ := exp.(time.Time)
	if expiresAt.IsZero() {
		// 兜底：claims 未带 exp（理论上不会发生），按当前配置 TTL 估一个上界。
		expiresAt = time.Now().Add(time.Duration(h.JWT.ttl))
	}
	_ = h.JWT.Revoke(c, jti, expiresAt)
	c.Status(http.StatusNoContent)
}
