"""实名认证 —— 姓名 + 身份证号 二要素核验

接阿里云 Cloudauth `Id2MetaVerify` API，返回 BizCode：
- 1: 姓名和身份证号一致 → 核验通过
- 2: 不一致
- 3: 无此身份证号
- 5: 姓名错误（身份证号存在但姓名不对）

测试号走白名单 mock（不扣真钱）：
- env `XIANGQIN_TEST_CERT_IDS`：逗号分隔的测试身份证号（命中直接 mock 通过）
- 对齐 issue #026 SMS 白名单模式

存储：
- 成功后 users.verified_at / cert_id_hash / legal_name 入库
- cert_id_hash HMAC-SHA256（独立 salt），明文号不落库
- UNIQUE(cert_id_hash) 防同一证件号多用户重注册（FEAT-033）
"""

from __future__ import annotations

import datetime as _dt
import logging
import os
import re
import uuid
from dataclasses import dataclass
from typing import Literal

import httpx

from . import credentials as creds
from .db import get_conn, hash_cert_id, now_ts
from .sms.aliyun import _rpc_sign  # 复用阿里云 RPC 签名（HMAC-SHA1 + base64）

log = logging.getLogger("xiangqin.realname")

ENDPOINT = "https://cloudauth.aliyuncs.com/"
API_VERSION = "2019-03-07"

# 中国大陆 18 位身份证格式（数字或末位 X）
CERT_ID_RE = re.compile(r"^\d{17}[\dXx]$")

# 业务码语义
BIZ_CODE_PASS = "1"
BIZ_CODE_MISMATCH = "2"
BIZ_CODE_UNKNOWN_ID = "3"
BIZ_CODE_NAME_WRONG = "5"

# FEAT-035 限流：24h 内累计失败 >= 3 次锁定
MAX_FAILED_ATTEMPTS = 3
LOCK_WINDOW_SECONDS = 24 * 3600


VerifyStatus = Literal["passed", "mismatch", "unknown_id", "name_wrong", "error"]


class RealnameError(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 VerifyResult:
    status: VerifyStatus
    biz_code: str | None  # 阿里云业务码，mock 通过时为 None
    message: str  # 人类可读
    verified: bool  # True 仅当 status == "passed"


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


def is_test_cert_id(cert_id: str) -> bool:
    """白名单测试证件号：命中直接 mock 通过，不调真 API（不扣钱）。"""
    return cert_id in _test_cert_id_whitelist()


def _validate_inputs(legal_name: str, cert_id: str) -> None:
    if not legal_name or not legal_name.strip():
        raise RealnameError("invalid_name", "姓名不能为空")
    if len(legal_name) > 64:
        raise RealnameError("invalid_name", "姓名过长")
    if not CERT_ID_RE.match(cert_id):
        raise RealnameError("invalid_cert_id", "身份证号格式错误（需 18 位）")


def _call_aliyun(legal_name: str, cert_id: str) -> dict:
    """真调阿里云 Cloudauth Id2MetaVerify。返回原始响应 dict。"""
    c = creds.get("aliyun")
    params = {
        "Format": "JSON",
        "Version": API_VERSION,
        "AccessKeyId": c["access_key_id"],
        "SignatureMethod": "HMAC-SHA1",
        "SignatureVersion": "1.0",
        "SignatureNonce": str(uuid.uuid4()),
        "Timestamp": _dt.datetime.now(_dt.UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "Action": "Id2MetaVerify",
        "ParamType": "normal",
        "UserName": legal_name,
        "IdentifyNum": cert_id,
    }
    params["Signature"] = _rpc_sign(params, c["access_key_secret"])

    try:
        r = httpx.get(ENDPOINT, params=params, timeout=15.0)
        r.raise_for_status()
        return r.json()
    except httpx.HTTPError as e:
        raise RealnameError(
            "aliyun_http_error", f"阿里云实人认证请求失败：{e}", http_status=503
        ) from e


def _translate_biz_code(biz_code: str | None) -> tuple[VerifyStatus, str]:
    """BizCode → (status, 人类可读消息)。"""
    match biz_code:
        case "1":
            return "passed", "实名核验通过"
        case "2":
            return "mismatch", "姓名和身份证号不一致"
        case "3":
            return "unknown_id", "无此身份证号"
        case "5":
            return "name_wrong", "姓名与身份证号绑定的姓名不符"
        case _:
            return "error", f"未知业务码 {biz_code!r}"


def verify(user_id: str, legal_name: str, cert_id: str) -> VerifyResult:
    """核验并在通过时写库。

    - 白名单测试号：直接 mock passed（不调真 API）
    - 其他号：真调阿里云 Id2MetaVerify
    - passed → UPDATE users.verified_at / cert_id_hash / legal_name
    - 同一 cert_id_hash 已被别人绑 → IntegrityError → RealnameError cert_id_bound

    不做：
    - session / 鉴权（调用方 assert_user_active）

    限流（FEAT-035）：24h 内失败 >= 3 次 → too_many_attempts (429)；成功 reset 计数。
    """
    _validate_inputs(legal_name, cert_id)
    _check_rate_limit(user_id)

    if is_test_cert_id(cert_id):
        log.info("[realname] test cert_id %s → mock passed", _mask(cert_id))
        status: VerifyStatus = "passed"
        biz_code = None
        message = "实名核验通过（测试白名单）"
    else:
        resp = _call_aliyun(legal_name, cert_id)
        code = resp.get("Code")
        if code != "200":
            # API 层错误（参数非法 / 未开通 / 限流）
            raise RealnameError(
                "aliyun_api_error",
                f"Cloudauth 错误 Code={code} Message={resp.get('Message')}",
                http_status=503,
            )
        biz_code = resp.get("ResultObject", {}).get("BizCode")
        status, message = _translate_biz_code(biz_code)

    verified = status == "passed"
    if verified:
        _persist_verified(user_id, legal_name, cert_id)
        _reset_failed_attempts(user_id)
    else:
        _record_failed_attempt(user_id)

    return VerifyResult(status=status, biz_code=biz_code, message=message, verified=verified)


def _check_rate_limit(user_id: str) -> None:
    """24h 内失败 >= MAX_FAILED_ATTEMPTS 次 → 抛 too_many_attempts (429)。"""
    with get_conn() as conn:
        row = conn.execute(
            "SELECT realname_failed_count, realname_last_failed_at FROM users WHERE id = ?",
            (user_id,),
        ).fetchone()
    if row is None:
        raise RealnameError("user_not_found", "用户不存在", http_status=404)
    count = row["realname_failed_count"] or 0
    last = row["realname_last_failed_at"]
    if count >= MAX_FAILED_ATTEMPTS and last and (now_ts() - last) < LOCK_WINDOW_SECONDS:
        retry_at = last + LOCK_WINDOW_SECONDS
        hrs = max(1, (retry_at - now_ts()) // 3600)
        raise RealnameError(
            "too_many_attempts",
            f"24 小时内实名尝试已达上限，请 {hrs} 小时后重试",
            http_status=429,
        )


def _record_failed_attempt(user_id: str) -> None:
    with get_conn() as conn:
        conn.execute(
            "UPDATE users "
            "SET realname_failed_count = COALESCE(realname_failed_count, 0) + 1, "
            "    realname_last_failed_at = ? "
            "WHERE id = ?",
            (now_ts(), user_id),
        )


def _reset_failed_attempts(user_id: str) -> None:
    with get_conn() as conn:
        conn.execute(
            "UPDATE users "
            "SET realname_failed_count = 0, realname_last_failed_at = NULL "
            "WHERE id = ?",
            (user_id,),
        )


def _persist_verified(user_id: str, legal_name: str, cert_id: str) -> None:
    h = hash_cert_id(cert_id)
    with get_conn() as conn:
        try:
            conn.execute(
                "UPDATE users SET verified_at = ?, cert_id_hash = ?, legal_name = ? WHERE id = ?",
                (now_ts(), h, legal_name, user_id),
            )
        except Exception as e:
            # sqlite3.IntegrityError on UNIQUE(cert_id_hash) → 别人已绑
            if "UNIQUE" in str(e) or "unique" in str(e).lower():
                raise RealnameError(
                    "cert_id_bound",
                    "此身份证号已被其他账号实名，一证一号",
                    http_status=409,
                ) from e
            raise


def assert_user_verified(user_id: str) -> None:
    """发信 / 被查询等闸门前调。未实名 → 抛 not_verified（FEAT-034 用）。"""
    with get_conn() as conn:
        row = conn.execute("SELECT verified_at FROM users WHERE id = ?", (user_id,)).fetchone()
    if row is None:
        raise RealnameError("user_not_found", "用户不存在", http_status=404)
    if not row["verified_at"]:
        raise RealnameError(
            "not_verified",
            "请先完成实名认证（xq auth verify-id <姓名> <身份证号>）",
            http_status=403,
        )


def is_verified(user_id: str) -> bool:
    """用于 query 结果带 verified 字段（决策点 #4 + #5）。"""
    with get_conn() as conn:
        row = conn.execute("SELECT verified_at FROM users WHERE id = ?", (user_id,)).fetchone()
    return bool(row and row["verified_at"])


def _mask(cert_id: str) -> str:
    """脱敏身份证号用于日志：前 4 + 后 4，中间 ****。"""
    if len(cert_id) < 10:
        return "****"
    return f"{cert_id[:4]}{'*' * (len(cert_id) - 8)}{cert_id[-4:]}"
