"""Auth 相关请求 / 响应 schemas。

跟客户端约定:
  - 字段统一用 snake_case
  - 时间字段统一 ISO 8601 (UTC)
  - 登录类接口成功时统一返回 TokenWithUser
"""
from __future__ import annotations

from datetime import datetime

from pydantic import BaseModel, ConfigDict, Field

# ===== 用户对外信息 =====

class UserOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)  # 允许直接 UserOut.model_validate(orm_user)

    id: int
    # 对外展示的账号 ID:11 位纯数字、首位非 1、全局唯一、创建时分配、不可变。区别于登录用的 phone。
    username: str
    phone: str
    nickname: str | None = None
    avatar_url: str | None = None
    register_channel: str
    status: str
    created_at: datetime
    last_login_at: datetime
    # 调试链接权限:前端据此在比价结果弹窗/记录页显示「复制调试链接」按钮。默认 false。
    debug_trace_enabled: bool = False


# ===== Token 通用结构 =====

class TokenPair(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "Bearer"
    expires_in: int = Field(..., description="access_token 剩余秒数")
    refresh_expires_in: int = Field(..., description="refresh_token 剩余秒数")


class TokenWithUser(TokenPair):
    user: UserOut
    onboarding_completed: bool = Field(
        False,
        description="该 设备+账号 是否已走完新手引导(据登录请求里的 device_id 计算)。"
        "true → 客户端登录后直接进首页,跳过引导。",
    )
    force_onboarding: bool = Field(
        False,
        description="是否强制本次登录走新手引导(仅测试号置 true)。客户端据此让【应用内重新登录】"
        "(退出后再登)也重走引导,普通号为 false、按原逻辑回原页/首页。与 onboarding_completed "
        "分工:后者管 App 启动路由,本字段管应用内登录后的去向。",
    )


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

class JverifyLoginRequest(BaseModel):
    login_token: str = Field(..., description="客户端 loginAuth 拿到的 loginToken", min_length=1)
    operator: str = Field("", description="CM/CU/CT,用于日志,可选")
    device_id: str = Field(
        "", max_length=64,
        description="硬件级设备标识(Android ANDROID_ID),用于新手引导按 设备+账号 去重;空=按未完成处理",
    )
    device_model: str = Field(
        "", max_length=128, description="客户端设备型号快照,用于登录安全审计"
    )


# ===== 短信验证码 =====

class SmsSendRequest(BaseModel):
    phone: str = Field(..., min_length=11, max_length=11, pattern=r"^1\d{10}$")
    device_id: str = Field(
        "", max_length=64,
        description="硬件级设备标识(Android ANDROID_ID),用于发码防刷按 设备+IP 限流;空=按 IP 聚一桶",
    )
    device_model: str = Field(
        "", max_length=128, description="客户端设备型号快照,用于短信安全审计"
    )


class SmsSendResponse(BaseModel):
    sent: bool
    mock: bool = Field(..., description="是否 mock 发送(true 时验证码不会真发,任意 6 位通过)")
    cooldown_sec: int = Field(..., description="多少秒后才能再发一次")


class SmsLoginRequest(BaseModel):
    phone: str = Field(..., min_length=11, max_length=11, pattern=r"^1\d{10}$")
    code: str = Field(..., min_length=4, max_length=8)
    device_id: str = Field(
        "", max_length=64,
        description="硬件级设备标识(Android ANDROID_ID),用于新手引导按 设备+账号 去重;空=按未完成处理",
    )
    device_model: str = Field(
        "", max_length=128, description="客户端设备型号快照,用于短信验证安全审计"
    )


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

class RefreshRequest(BaseModel):
    refresh_token: str = Field(..., min_length=1)


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

class LogoutResponse(BaseModel):
    ok: bool = True


# ===== 微信登录 =====

class WechatLoginRequest(BaseModel):
    code: str = Field(..., min_length=1, description="微信 App 授权拿到的 code(单次有效)")
    device_id: str = Field(
        "", max_length=64,
        description="硬件级设备标识(Android ANDROID_ID),用于新手引导按 设备+账号 去重;空=按未完成处理",
    )


class WechatLoginResponse(BaseModel):
    # status="logged_in" → openid 命中,token 有值;"need_bind_phone" → 未命中,bind_ticket 有值
    status: str
    token: TokenWithUser | None = None
    bind_ticket: str | None = None
    wechat_nickname: str | None = None
    wechat_avatar_url: str | None = None


class OccupiedAccountInfo(BaseModel):
    """手机号被占用时返回的原账号脱敏展示信息(供冲突页)。"""
    nickname: str | None = None
    avatar_url: str | None = None
    created_at: datetime
    has_wechat: bool = False


class WechatBindResultResponse(BaseModel):
    # status="logged_in" → 未占用,已建号登入,token 有值;
    # "phone_occupied" → 手机号被占用,occupied_account + conflict_ticket 有值,token 为 None
    status: str
    token: TokenWithUser | None = None
    occupied_account: OccupiedAccountInfo | None = None
    conflict_ticket: str | None = None            # 占用时签发,换绑/继续绑定只认它
    rebind_available: bool | None = None          # 该手机号 30 天内是否还能换绑(给换绑按钮预置禁用态)
    rebind_blocked_days: int | None = None        # 被限时剩余天数(rebind_available=False 时>0)


class WechatBindPhoneSmsRequest(BaseModel):
    bind_ticket: str = Field(..., min_length=1)
    phone: str = Field(..., min_length=11, max_length=11, pattern=r"^1\d{10}$")
    code: str = Field(..., min_length=4, max_length=8)
    device_id: str = Field("", max_length=64)


class WechatBindPhoneJverifyRequest(BaseModel):
    bind_ticket: str = Field(..., min_length=1)
    login_token: str = Field(..., min_length=1, description="客户端 loginAuth 拿到的 loginToken")
    device_id: str = Field("", max_length=64)


class WechatConflictContinueRequest(BaseModel):
    conflict_ticket: str = Field(..., min_length=1)
    device_id: str = Field("", max_length=64)


class WechatConflictRebindRequest(BaseModel):
    conflict_ticket: str = Field(..., min_length=1)
    device_id: str = Field("", max_length=64)
