"""注册 / 验证码 / session 业务逻辑。

设计红线:
- 手机号从头到尾不落明文（stdout / DB / 日志）；仅在 register 入口内存持有一次
- 验证码通过 xiangqin.sms provider 发送：
    - mock provider：code 恒为 `123456`（dev / demo）
    - aliyun provider：code 随机 6 位，真调阿里云短信
  选 provider 走环境变量 XIANGQIN_SMS_PROVIDER（默认 mock）
- session_token 只在 verify 响应里明文回一次；后续全走 hash 比对
"""

from __future__ import annotations

import os
import re
import secrets
from dataclasses import dataclass

from . import sms
from .db import (
    get_conn,
    hash_code,
    hash_phone,
    hash_token,
    new_id,
    now_ts,
)

# mock provider 固定验证码（由 mock.py 契约保证一致）
MOCK_CODE = "123456"
REQUEST_TTL_S = 60
SESSION_TTL_S = 30 * 24 * 3600  # 30d

PHONE_RE = re.compile(r"^1[3-9]\d{9}$")


def _test_phone_whitelist() -> set[str]:
    raw = os.environ.get("XIANGQIN_TEST_PHONES", "").strip()
    return {p.strip() for p in raw.split(",") if p.strip()} if raw else set()


def _test_otp() -> str:
    """staging E2E 用固定 code；prod 留空 = 整个白名单机制关闭。"""
    return os.environ.get("XIANGQIN_TEST_OTP", "").strip()


def is_test_phone(phone: str) -> bool:
    """白名单命中且 TEST_OTP 非空才算启用（双保险，prod 默认关）。"""
    return bool(_test_otp()) and phone in _test_phone_whitelist()


def _pick_code(phone: str) -> str:
    """挑验证码：
    - 测试号白名单命中（且 TEST_OTP 配置）→ 固定 TEST_OTP（staging/prod 都能用）
    - provider=mock → 123456（dev）
    - provider=aliyun → 6 位随机
    """
    if is_test_phone(phone):
        return _test_otp()
    provider = os.environ.get("XIANGQIN_SMS_PROVIDER", "mock").lower()
    if provider == "mock":
        return MOCK_CODE
    return f"{secrets.randbelow(10**6):06d}"


class AuthError(Exception):
    """认证 / 验证失败。code 字段用于 API 响应。"""

    def __init__(self, code: str, msg: str, http_status: int = 400):
        super().__init__(msg)
        self.code = code
        self.http_status = http_status


@dataclass
class RegisterResult:
    request_id: str
    masked_phone: str
    expires_in_s: int


@dataclass
class VerifyResult:
    session_token: str
    user_id: str
    is_new_user: bool


def register(phone: str) -> RegisterResult:
    """发起注册 / 登录。同一个手机号有活跃 request 时复用。

    0.1 期 mock：code 永远是 `123456`，但仍按流程走 hash + expires_at。
    """
    if not PHONE_RE.match(phone):
        raise AuthError("invalid_phone", "手机号格式错误（仅支持大陆 11 位）")

    phone_h = hash_phone(phone)
    code = _pick_code(phone)
    code_h = hash_code(code)
    now = now_ts()
    expires = now + REQUEST_TTL_S
    request_id = new_id()

    with get_conn() as conn:
        # 复用未过期未使用的 request（60s 内重复点"发送"不产生新 request）
        row = conn.execute(
            "SELECT request_id FROM auth_requests "
            "WHERE phone_hash = ? AND used = 0 AND expires_at > ? "
            "ORDER BY expires_at DESC LIMIT 1",
            (phone_h, now),
        ).fetchone()
        if row is not None:
            ttl = (
                conn.execute(
                    "SELECT expires_at FROM auth_requests WHERE request_id = ?",
                    (row["request_id"],),
                ).fetchone()["expires_at"]
                - now
            )
            return RegisterResult(
                request_id=row["request_id"],
                masked_phone=_mask(phone),
                expires_in_s=max(0, ttl),
            )

        conn.execute(
            "INSERT INTO auth_requests (request_id, phone_hash, code_hash, expires_at, used) "
            "VALUES (?, ?, ?, ?, 0)",
            (request_id, phone_h, code_h, expires),
        )

    # 白名单测试号不调 SMS provider（省钱 + 避免打扰真 SMS 服务）
    if not is_test_phone(phone):
        try:
            sms.get_provider().send_code(phone, code)
        except Exception as e:
            raise AuthError("sms_send_failed", f"验证码发送失败：{e}", http_status=503) from e

    return RegisterResult(
        request_id=request_id,
        masked_phone=_mask(phone),
        expires_in_s=REQUEST_TTL_S,
    )


def verify(request_id: str, code: str) -> VerifyResult:
    """用 request_id + 验证码换 session_token。"""
    if not request_id or not code:
        raise AuthError("invalid_value", "request_id 和 code 均必填")

    code_h = hash_code(code)
    now = now_ts()

    with get_conn() as conn:
        req = conn.execute(
            "SELECT request_id, phone_hash, code_hash, expires_at, used "
            "FROM auth_requests WHERE request_id = ?",
            (request_id,),
        ).fetchone()

        if req is None:
            raise AuthError("invalid_request_id", "request_id 不存在")
        if req["used"]:
            raise AuthError("code_already_used", "该验证码已使用")
        if req["expires_at"] < now:
            raise AuthError("code_expired", "验证码已过期（60s 有效）")
        if req["code_hash"] != code_h:
            raise AuthError("invalid_code", "验证码错误")

        conn.execute(
            "UPDATE auth_requests SET used = 1 WHERE request_id = ?",
            (request_id,),
        )

        phone_h = req["phone_hash"]
        user_row = conn.execute(
            "SELECT id FROM users WHERE phone_hash = ? AND deleted_at IS NULL",
            (phone_h,),
        ).fetchone()

        if user_row is None:
            user_id = new_id()
            conn.execute(
                "INSERT INTO users (id, phone_hash, created_at) VALUES (?, ?, ?)",
                (user_id, phone_h, now),
            )
            conn.execute(
                "INSERT INTO profiles (user_id, updated_at) VALUES (?, ?)",
                (user_id, now),
            )
            is_new = True
        else:
            user_id = user_row["id"]
            is_new = False

        session_token = _new_session_token()
        conn.execute(
            "INSERT INTO sessions (token_hash, user_id, created_at, last_seen_at) "
            "VALUES (?, ?, ?, ?)",
            (hash_token(session_token), user_id, now, now),
        )

    return VerifyResult(
        session_token=session_token,
        user_id=user_id,
        is_new_user=is_new,
    )


def resolve_session(session_token: str) -> str:
    """从 session_token 返回 user_id（触底更新 last_seen_at）。失败抛 AuthError."""
    if not session_token:
        raise AuthError("unauthenticated", "缺少 session_token", http_status=401)

    th = hash_token(session_token)
    now = now_ts()
    with get_conn() as conn:
        row = conn.execute(
            "SELECT user_id, created_at FROM sessions WHERE token_hash = ?",
            (th,),
        ).fetchone()
        if row is None:
            raise AuthError("unauthenticated", "session 无效或已登出", http_status=401)
        if now - row["created_at"] > SESSION_TTL_S:
            conn.execute("DELETE FROM sessions WHERE token_hash = ?", (th,))
            raise AuthError("unauthenticated", "session 已过期", http_status=401)
        conn.execute(
            "UPDATE sessions SET last_seen_at = ? WHERE token_hash = ?",
            (now, th),
        )
        return row["user_id"]


def logout(session_token: str) -> None:
    """删除 session 行。token 无效时幂等返回 OK。"""
    if not session_token:
        return
    th = hash_token(session_token)
    with get_conn() as conn:
        conn.execute("DELETE FROM sessions WHERE token_hash = ?", (th,))


def _mask(phone: str) -> str:
    return f"{phone[:3]}****{phone[-4:]}"


def _new_session_token() -> str:
    """URL-safe 128-bit 随机。"""
    return secrets.token_urlsafe(24)
