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

设计红线:
- 手机号从头到尾不落明文（stdout / DB / 日志）；仅在 register 入口内存持有一次
- 验证码 0.1 期 mock 固定 `123456`（M2.3 记 TODO 对接阿里云短信）
- session_token 只在 verify 响应里明文回一次；后续全走 hash 比对
"""

from __future__ import annotations

import re
import secrets
from dataclasses import dataclass

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

# 0.1 期固定 mock 验证码
MOCK_CODE = "123456"
REQUEST_TTL_S = 60
SESSION_TTL_S = 30 * 24 * 3600  # 30d

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


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_h = hash_code(MOCK_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),
        )

    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),
            )
            conn.execute(
                "INSERT INTO exposure_usage (user_id) VALUES (?)",
                (user_id,),
            )
            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)
