"""认证相关 endpoint。

路由前缀 `/api/v1/auth`,包含:
  POST /jverify-login         极光一键登录(loginToken → 手机号 → 注册即登录 → 签 JWT)
  POST /sms/send              发短信验证码(mock 阶段任意 6 位通过)
  POST /sms/login             手机号 + 验证码登录
  POST /refresh               用 refresh_token 换新的 token 对
  GET  /me                    当前登录用户(Bearer access_token)
  POST /logout                客户端登出(目前服务端无状态,仅返回 ok。后续加 jti 黑名单)
"""
from __future__ import annotations

import logging

from fastapi import APIRouter, Depends, HTTPException, Request, status

from app.api.deps import CurrentUser, DbSession
from app.core import test_account
from app.core.ratelimit import enforce_rate_limit, rate_limit
from app.core.security import TokenError, decode_token, issue_token_pair
from app.integrations.jiguang import JiguangError, mask_phone, verify_and_get_phone
from app.integrations.sms import SmsError, send_code, verify_code
from app.repositories import onboarding as onboarding_repo
from app.repositories import user as user_repo
from app.schemas.auth import (
    JverifyLoginRequest,
    LogoutResponse,
    RefreshRequest,
    SmsLoginRequest,
    SmsSendRequest,
    SmsSendResponse,
    TokenPair,
    TokenWithUser,
    UserOut,
)

logger = logging.getLogger("shagua.auth")

router = APIRouter(prefix="/api/v1/auth", tags=["auth"])

# 手机号登录防刷:同一设备(device_id) + 同一 IP 每小时最多的登录尝试次数(成功/失败都计)。
SMS_LOGIN_MAX_PER_HOUR = 5
# 发码防刷:同一设备(device_id) + 同一 IP 每小时最多的发码次数。
# 堵「换手机号绕开单号 60s 冷却 / 单号每日上限」的洞 —— 那两道是单号维度,一机换号能绕开。
SMS_SEND_MAX_PER_HOUR_PER_DEVICE = 5


def _login_response(
    user, *, onboarding_completed: bool, force_onboarding: bool = False
) -> TokenWithUser:
    """登录类接口共用的响应组装。onboarding_completed 据登录请求的 device_id 算好后传入;
    force_onboarding 仅测试号置 True(让应用内重新登录也重走引导)。"""
    tokens = issue_token_pair(user.id)
    return TokenWithUser(
        **tokens,
        user=UserOut.model_validate(user),
        onboarding_completed=onboarding_completed,
        force_onboarding=force_onboarding,
    )


# ===================== 极光一键登录 =====================

@router.post("/jverify-login", response_model=TokenWithUser, summary="极光一键登录")
def jverify_login(req: JverifyLoginRequest, db: DbSession) -> TokenWithUser:
    logger.info(
        "jverify_login operator=%s token_len=%d",
        req.operator or "-",
        len(req.login_token),
    )

    try:
        phone = verify_and_get_phone(req.login_token)
    except JiguangError as e:
        logger.error("[JG] verify+decrypt failed: %s", e, exc_info=True)
        raise HTTPException(status_code=502, detail=f"jiguang verify failed: {e}") from e

    user = user_repo.upsert_user_for_login(db, phone=phone, register_channel="jverify")
    if user.status != "active":
        raise HTTPException(status_code=403, detail="account disabled")

    completed = onboarding_repo.is_completed(db, user_id=user.id, device_id=req.device_id)
    logger.info("jverify_login ok user_id=%d phone=%s onboarded=%s", user.id, mask_phone(phone), completed)
    return _login_response(user, onboarding_completed=completed)


# ===================== 短信登录 =====================

@router.post(
    "/sms/send",
    response_model=SmsSendResponse,
    summary="发送短信验证码",
)
def sms_send(req: SmsSendRequest, request: Request) -> SmsSendResponse:
    # 测试账号:不真发短信(号码非真实手机号,真发会失败/浪费),直接放行让客户端进入填码界面。
    # 真正的"免验证码"在 /sms/login 跳过校验;每日上限也在 login 处算(send 不耗额度)。
    # (也不受下面设备发码限流约束:QA 联调要反复发码。)
    if test_account.is_test_account(req.phone):
        logger.info("test_account sms_send short-circuit (不真发)")
        return SmsSendResponse(sent=True, mock=True, cooldown_sec=0)

    # 防刷:同一设备(device_id) + 同一 IP 每小时最多 SMS_SEND_MAX_PER_HOUR_PER_DEVICE 次发码。
    # 补「换手机号绕开单号 60s 冷却 / 单号每日上限」的洞(那两道是单号维度,一机换号能绕);设备维度按机器封顶,
    # 挡短信轰炸/烧钱。放在真发(send_code)之前 → 超限直接拦下、不真发短信。与路由上 IP 维度(10次/分钟)互补。
    enforce_rate_limit(
        request,
        scope="sms-send-device",
        subject=req.device_id,
        limit=SMS_SEND_MAX_PER_HOUR_PER_DEVICE,
        window_sec=3600,
        detail="操作过于频繁,请稍后再试",
    )

    try:
        cooldown = send_code(req.phone)
    except SmsError as e:
        raise HTTPException(status_code=e.status_code, detail=str(e)) from e

    from app.core.config import settings  # 局部 import 避免循环

    return SmsSendResponse(sent=True, mock=settings.SMS_MOCK, cooldown_sec=cooldown)


@router.post(
    "/sms/login",
    response_model=TokenWithUser,
    summary="手机号+验证码登录",
    dependencies=[Depends(rate_limit(20, 60, "sms-login"))],  # 防撞库爆破(另有单码失败次数上限)
)
def sms_login(req: SmsLoginRequest, request: Request, db: DbSession) -> TokenWithUser:
    # 测试账号:免验证码登录 + 每日上限 + 每次都走新手引导(详见 app/core/test_account.py)。
    # 放在最前面:命中即不校验验证码;先扣当日额度,超限直接拒,挡住有人猜到号后脚本刷。
    # 测试账号走自己的每日额度、不受下面 (设备+IP) 每小时限流约束(QA 需在一小时内反复登录联调)。
    if test_account.is_test_account(req.phone):
        if not test_account.try_consume_quota():
            raise HTTPException(status_code=429, detail="测试账号今日使用次数已达上限,请明天再试")
        user = user_repo.upsert_user_for_login(db, phone=req.phone, register_channel="sms")
        if user.status != "active":
            raise HTTPException(status_code=403, detail="account disabled")
        logger.info("sms_login test_account ok user_id=%d phone=%s", user.id, mask_phone(req.phone))
        # onboarding 恒 false + force_onboarding=true:每次登录都重走引导(含应用内退出后重登),
        # 不读/不写 onboarding_completion 表。
        return _login_response(user, onboarding_completed=False, force_onboarding=True)

    # 防刷:同一设备(device_id) + 同一 IP 每小时最多 SMS_LOGIN_MAX_PER_HOUR 次登录尝试。放在验证码校验
    # **之前** → 输错验证码的失败尝试也计数,才挡得住撞库/爆破。与路由上 IP 维度的 sms-login 限流(同 IP)互补。
    # ⚠️ 按设备而非手机号 → 一台机器换不同手机号刷登录也受限(防一机狂登多号);device_id 空(老客户端)时
    #    退化为该 IP 下所有空设备聚一桶,仍受限。
    enforce_rate_limit(
        request,
        scope="sms-login-device",
        subject=req.device_id,
        limit=SMS_LOGIN_MAX_PER_HOUR,
        window_sec=3600,
        detail="登录尝试过于频繁,请稍后再试",
    )

    if not verify_code(req.phone, req.code):
        raise HTTPException(status_code=400, detail="invalid sms code")

    user = user_repo.upsert_user_for_login(db, phone=req.phone, register_channel="sms")
    if user.status != "active":
        raise HTTPException(status_code=403, detail="account disabled")

    completed = onboarding_repo.is_completed(db, user_id=user.id, device_id=req.device_id)
    logger.info("sms_login ok user_id=%d phone=%s onboarded=%s", user.id, mask_phone(req.phone), completed)
    return _login_response(user, onboarding_completed=completed)


# ===================== Refresh =====================

@router.post("/refresh", response_model=TokenPair, summary="用 refresh_token 换新 token 对")
def refresh(req: RefreshRequest, db: DbSession) -> TokenPair:
    try:
        payload = decode_token(req.refresh_token, expected_type="refresh")
    except TokenError as e:
        raise HTTPException(status_code=401, detail=str(e)) from e

    user_id = int(payload["sub"])
    user = user_repo.get_user_by_id(db, user_id)
    if user is None or user.status != "active":
        raise HTTPException(status_code=401, detail="user not found or disabled")

    return TokenPair(**issue_token_pair(user.id))


# ===================== 当前用户 =====================

@router.get("/me", response_model=UserOut, summary="获取当前登录用户")
def me(user: CurrentUser) -> UserOut:
    return UserOut.model_validate(user)


# ===================== Logout =====================

@router.post("/logout", response_model=LogoutResponse, summary="登出(占位,客户端清 token)")
def logout(user: CurrentUser) -> LogoutResponse:
    # 当前服务端无状态,真正的 token 失效靠客户端删 token。
    # TODO: 加 jti 黑名单表后,这里把 access_token 的 jti 写黑名单
    logger.info("logout user_id=%d", user.id)
    return LogoutResponse(ok=True)
