"""sqlite schema + 连接 + 基础工具。

设计：
- 所有身份和凭证以 hash 形式入库（phone / session_token / 验证码）
- WAL 模式（并发读写 + 崩溃恢复）
- tags 字段 pipe 包裹格式 `|a|b|c|`，LIKE 查包含
- exposure_usage.remaining > 0 等于"在付费池"

表清单：
- users              身份锚点（phone_hash 唯一）
- profiles           可展示字段 + agent gateway 配置
- auth_requests      短信验证码临时存储
- sessions           登录态（token_hash → user_id）
- messages           虚拟信封私信
- envelope_usage     每日信封额度消耗
- envelope_balance   付费信封余额
- envelope_orders    买信封的订单流水
- reports            举报留档（永久）
- blocks             拉黑关系
- notify_state       通知去重状态（last_notified_at + last_read_inbox_at）

已废弃的表（生产迁移时 DROP）：
- exposure_orders / exposure_usage（付费曝光系统，2026-04-23 砍）
"""

from __future__ import annotations

import hashlib
import hmac
import os
import sqlite3
import time
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator

import ulid as ulid_lib  # type: ignore

DEFAULT_DB_PATH = Path(os.environ.get("XIANGQIN_DB_PATH", "/opt/xiangqin/data/xiangqin.db"))
PHONE_SALT = os.environ.get("XIANGQIN_PHONE_SALT", "dev-salt-not-for-prod").encode()
CERT_ID_SALT = os.environ.get("XIANGQIN_CERT_ID_SALT", "dev-salt-not-for-prod").encode()

SCHEMA = """
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA synchronous = NORMAL;

CREATE TABLE IF NOT EXISTS users (
    id          TEXT PRIMARY KEY,
    phone_hash  TEXT UNIQUE NOT NULL,
    created_at  INTEGER NOT NULL,
    deleted_at  INTEGER,
    -- FEAT-025 封号：active（默认）/ frozen（冷静期，不能发信可看）/ banned（硬封）
    status      TEXT NOT NULL DEFAULT 'active'
                    CHECK(status IN ('active', 'frozen', 'banned')),
    status_reason   TEXT,
    status_updated_at   INTEGER
);

CREATE TABLE IF NOT EXISTS profiles (
    user_id             TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    gender              TEXT CHECK(gender IN ('f','m','nb')),
    age                 INTEGER CHECK(age IS NULL OR (age >= 18 AND age <= 99)),
    city                TEXT,
    tags                TEXT,
    bio                 TEXT,
    height              INTEGER CHECK(height IS NULL OR (height >= 140 AND height <= 220)),
    education           TEXT CHECK(education IS NULL OR education IN
                            ('highschool','associate','bachelor','master','phd','other')),
    agent_gateway_url   TEXT,
    agent_hooks_token   TEXT,
    notify_on_new_mail  INTEGER NOT NULL DEFAULT 0 CHECK(notify_on_new_mail IN (0,1)),
    updated_at          INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_profiles_gender_city_age
    ON profiles(gender, city, age);
CREATE INDEX IF NOT EXISTS idx_profiles_height
    ON profiles(height) WHERE height IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_profiles_education
    ON profiles(education) WHERE education IS NOT NULL;

CREATE TABLE IF NOT EXISTS auth_requests (
    request_id      TEXT PRIMARY KEY,
    phone_hash      TEXT NOT NULL,
    code_hash       TEXT NOT NULL,
    expires_at      INTEGER NOT NULL,
    used            INTEGER NOT NULL DEFAULT 0 CHECK(used IN (0,1))
);

CREATE INDEX IF NOT EXISTS idx_auth_expires ON auth_requests(expires_at);

CREATE TABLE IF NOT EXISTS sessions (
    token_hash      TEXT PRIMARY KEY,
    user_id         TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    created_at      INTEGER NOT NULL,
    last_seen_at    INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);

-- ========== M4: 虚拟信封邮件模型 ==========

CREATE TABLE IF NOT EXISTS messages (
    id                          TEXT PRIMARY KEY,
    from_user_id                TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    to_user_id                  TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    body                        TEXT NOT NULL,
    sent_at                     INTEGER NOT NULL,
    delivery_status             TEXT NOT NULL
        CHECK(delivery_status IN ('delivered','blocked_by_recipient',
                                  'recipient_quota_full','recipient_not_found')),
    delivery_failure_reason     TEXT,
    notify_attempted_at         INTEGER,
    notify_status               TEXT CHECK(notify_status IS NULL OR notify_status IN
                                    ('not_enabled','pushed','push_failed','suppressed_dup')),
    notify_error                TEXT,
    read_at                     INTEGER,
    replied_at                  INTEGER,
    reply_to_message_id         TEXT,
    deleted_by_recipient_at     INTEGER,
    envelope_source             TEXT NOT NULL CHECK(envelope_source IN ('free_daily','paid')),
    expires_at                  INTEGER NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_messages_to_unread
    ON messages(to_user_id, read_at, sent_at DESC)
    WHERE deleted_by_recipient_at IS NULL AND delivery_status = 'delivered';
CREATE INDEX IF NOT EXISTS idx_messages_from
    ON messages(from_user_id, sent_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_expires
    ON messages(expires_at);

-- 每用户当日信封使用量 + 累计付费余额
-- day_key 是 UTC 日期整数 'YYYYMMDD'（按 UTC 算；跨时区不同步但足够简单）
CREATE TABLE IF NOT EXISTS envelope_usage (
    user_id         TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    day_key         INTEGER NOT NULL,
    free_used       INTEGER NOT NULL DEFAULT 0 CHECK(free_used >= 0),
    paid_used       INTEGER NOT NULL DEFAULT 0 CHECK(paid_used >= 0),
    PRIMARY KEY (user_id, day_key)
);

-- 付费信封余额（独立于每日计数；买了就入，任何一天都可用）
CREATE TABLE IF NOT EXISTS envelope_balance (
    user_id         TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    paid_remaining  INTEGER NOT NULL DEFAULT 0 CHECK(paid_remaining >= 0),
    total_purchased INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE IF NOT EXISTS envelope_orders (
    id              TEXT PRIMARY KEY,
    user_id         TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    count           INTEGER NOT NULL CHECK(count > 0),
    amount_cents    INTEGER NOT NULL CHECK(amount_cents >= 0),
    mock            INTEGER NOT NULL CHECK(mock IN (0,1)),
    paid            INTEGER NOT NULL CHECK(paid IN (0,1)),
    created_at      INTEGER NOT NULL,
    provider        TEXT,
    biz_order_id    TEXT,
    status          TEXT NOT NULL DEFAULT 'paid'
                         CHECK(status IN ('pending','paid','failed','cancelled'))
);

CREATE INDEX IF NOT EXISTS idx_envelope_orders_user
    ON envelope_orders(user_id, created_at DESC);

CREATE TABLE IF NOT EXISTS reports (
    id              TEXT PRIMARY KEY,
    message_id      TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    reporter_id     TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    reported_id     TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    reason          TEXT,
    created_at      INTEGER NOT NULL,
    -- FEAT-028/029 举报审核：pending（默认）/ confirmed / rejected / passed
    review_status   TEXT NOT NULL DEFAULT 'pending'
                        CHECK(review_status IN ('pending', 'confirmed', 'rejected', 'passed')),
    reviewed_by     TEXT,
    reviewed_at     INTEGER
);

CREATE INDEX IF NOT EXISTS idx_reports_reported
    ON reports(reported_id, created_at DESC);
-- idx_reports_review_status: 旧库升级时 review_status 列要先 ALTER 加上才能建索引，
-- 因此这条索引移到 _migrate_reports_review() 函数里执行（SCHEMA 这里不建）。

CREATE TABLE IF NOT EXISTS blocks (
    blocker_id      TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    blocked_id      TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    created_at      INTEGER NOT NULL,
    PRIMARY KEY (blocker_id, blocked_id)
);

CREATE INDEX IF NOT EXISTS idx_blocks_blocked
    ON blocks(blocked_id);

-- 通知去重状态（每用户一行）
CREATE TABLE IF NOT EXISTS notify_state (
    user_id             TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    last_notified_at    INTEGER,
    last_read_inbox_at  INTEGER
);

-- FEAT-008 / FEAT-009: 照片上传（头像 + 相册）
CREATE TABLE IF NOT EXISTS user_photos (
    id                  TEXT PRIMARY KEY,
    user_id             TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    purpose             TEXT NOT NULL CHECK(purpose IN ('avatar', 'album', 'idcard')),
    oss_key             TEXT NOT NULL,
    sha256              TEXT NOT NULL,
    content_type        TEXT NOT NULL,
    size_bytes          INTEGER NOT NULL,
    uploaded_at         INTEGER NOT NULL,
    moderation_status   TEXT NOT NULL DEFAULT 'pending'
                            CHECK(moderation_status IN ('pending', 'passed', 'rejected')),
    moderation_reason   TEXT
);
CREATE INDEX IF NOT EXISTS idx_user_photos_user_purpose
    ON user_photos(user_id, purpose);
"""


def hash_phone(phone: str) -> str:
    """HMAC-SHA256(phone) → hex。SALT 走 env var。"""
    return hmac.new(PHONE_SALT, phone.encode(), hashlib.sha256).hexdigest()


def hash_cert_id(cert_id: str) -> str:
    """HMAC-SHA256(身份证号) → hex。SALT 独立于 phone（避免 hash 复用打穿）。"""
    return hmac.new(CERT_ID_SALT, cert_id.encode(), hashlib.sha256).hexdigest()


def hash_code(code: str) -> str:
    """验证码 sha256（不加盐，验证码本身就是短期随机）。"""
    return hashlib.sha256(code.encode()).hexdigest()


def hash_token(token: str) -> str:
    """session_token sha256。"""
    return hashlib.sha256(token.encode()).hexdigest()


def new_id() -> str:
    """ULID 字符串，时序 + 随机。"""
    return str(ulid_lib.new())


def now_ts() -> int:
    return int(time.time())


def _resolve_path(path: Path | None) -> Path:
    """运行时动态查 DEFAULT_DB_PATH（测试可覆写 module attr）。"""
    if path is not None:
        return path
    return DEFAULT_DB_PATH


def init_db(path: Path | None = None) -> None:
    """建目录 + 执行 schema（幂等）。含已建表的列级 migration。"""
    path = _resolve_path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(path) as conn:
        conn.executescript(SCHEMA)
        _migrate_profile_columns(conn)
        _migrate_users_status(conn)
        _migrate_reports_review(conn)
        _migrate_users_realname(conn)
        _drop_exposure_tables(conn)


def _drop_exposure_tables(conn: sqlite3.Connection) -> None:
    """2026-04-23 砍曝光系统：DROP 生产老库里的 exposure_orders / exposure_usage。幂等。"""
    conn.execute("DROP TABLE IF EXISTS exposure_orders")
    conn.execute("DROP TABLE IF EXISTS exposure_usage")


def _migrate_users_realname(conn: sqlite3.Connection) -> None:
    """0.5→0.6 迁移：users 表加 verified_at / cert_id_hash / legal_name（FEAT-033 实名认证）。"""
    cols = {r[1] for r in conn.execute("PRAGMA table_info(users)")}
    if "verified_at" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN verified_at INTEGER")
    if "cert_id_hash" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN cert_id_hash TEXT")
    if "legal_name" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN legal_name TEXT")
    # FEAT-035：限流字段
    if "realname_failed_count" not in cols:
        conn.execute(
            "ALTER TABLE users ADD COLUMN realname_failed_count INTEGER NOT NULL DEFAULT 0"
        )
    if "realname_last_failed_at" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN realname_last_failed_at INTEGER")
    # cert_id_hash 唯一索引：同一身份证号只能绑一个 user（防一人多注册刷福利）
    # partial index：null 不计入唯一约束
    conn.execute(
        "CREATE UNIQUE INDEX IF NOT EXISTS idx_users_cert_id_hash "
        "ON users(cert_id_hash) WHERE cert_id_hash IS NOT NULL"
    )


def _migrate_users_status(conn: sqlite3.Connection) -> None:
    """0.5→0.6 迁移：users 表加 status / status_reason / status_updated_at（FEAT-025 封号）。"""
    cols = {r[1] for r in conn.execute("PRAGMA table_info(users)")}
    if "status" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active'")
    if "status_reason" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN status_reason TEXT")
    if "status_updated_at" not in cols:
        conn.execute("ALTER TABLE users ADD COLUMN status_updated_at INTEGER")


def _migrate_reports_review(conn: sqlite3.Connection) -> None:
    """0.5→0.6 迁移：reports 表加 review_status / reviewed_by / reviewed_at（FEAT-028/029 审核）。"""
    cols = {r[1] for r in conn.execute("PRAGMA table_info(reports)")}
    if "review_status" not in cols:
        conn.execute("ALTER TABLE reports ADD COLUMN review_status TEXT NOT NULL DEFAULT 'pending'")
    if "reviewed_by" not in cols:
        conn.execute("ALTER TABLE reports ADD COLUMN reviewed_by TEXT")
    if "reviewed_at" not in cols:
        conn.execute("ALTER TABLE reports ADD COLUMN reviewed_at INTEGER")
    conn.execute(
        "CREATE INDEX IF NOT EXISTS idx_reports_review_status ON reports(review_status, created_at DESC)"
    )


def _migrate_profile_columns(conn: sqlite3.Connection) -> None:
    """0.3→0.4 迁移：给老 profiles 表加 M4 字段；0.4→0.5 加 avatar_photo_id。"""
    cols = {r[1] for r in conn.execute("PRAGMA table_info(profiles)")}
    if "height" not in cols:
        conn.execute("ALTER TABLE profiles ADD COLUMN height INTEGER")
    if "education" not in cols:
        conn.execute("ALTER TABLE profiles ADD COLUMN education TEXT")
    if "agent_gateway_url" not in cols:
        conn.execute("ALTER TABLE profiles ADD COLUMN agent_gateway_url TEXT")
    if "agent_hooks_token" not in cols:
        conn.execute("ALTER TABLE profiles ADD COLUMN agent_hooks_token TEXT")
    if "notify_on_new_mail" not in cols:
        conn.execute(
            "ALTER TABLE profiles ADD COLUMN notify_on_new_mail INTEGER NOT NULL DEFAULT 0"
        )
    if "avatar_photo_id" not in cols:
        conn.execute("ALTER TABLE profiles ADD COLUMN avatar_photo_id TEXT")
    # 列齐备后建索引
    conn.execute(
        "CREATE INDEX IF NOT EXISTS idx_profiles_height ON profiles(height) "
        "WHERE height IS NOT NULL"
    )
    conn.execute(
        "CREATE INDEX IF NOT EXISTS idx_profiles_education ON profiles(education) "
        "WHERE education IS NOT NULL"
    )


@contextmanager
def get_conn(path: Path | None = None) -> Iterator[sqlite3.Connection]:
    """Context manager：返回 Row factory + foreign_keys 已开的连接。"""
    path = _resolve_path(path)
    conn = sqlite3.connect(path)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA foreign_keys = ON")
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()


def mask_phone(phone: str) -> str:
    """138****1111 —— 脱敏，永远不要输出完整手机号。"""
    if len(phone) < 7:
        return "***"
    return f"{phone[:3]}****{phone[-4:]}"
