"""虚拟信封邮件模型（M4）。

核心：信箱 + 通知分离。信箱是基础设施（所有人都有），通知是可选上层。

职责分工：
- send():    A 发信 → 扣信封 → 落 messages → 触发通知（去重）
- deliver(): 内部投递路径，检查拉黑 / 记录状态
- notify():  通过 agent_gateway_url POST /hooks/agent 推送
- list_inbox() / list_sent(): 查收件 / 发件
- read_message() / delete_message() / reply(): 收件方动作
- report() / block() / unblock() / is_blocked(): 举报 / 拉黑
- buy_envelopes() / get_envelope_state(): 信封配额管理
"""

from __future__ import annotations

import datetime as _dt
import logging
import time
from dataclasses import dataclass
from typing import Any

import httpx

from .db import get_conn, new_id, now_ts

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

# ========== 常量（可调，真实上线后按数据迭代） ==========

BODY_MAX_LEN = 2000
MESSAGE_TTL_DAYS = 30
FREE_ENVELOPES_PER_DAY = 3
ENVELOPE_PRICE_CENTS = 100  # ¥1.00 per envelope
NOTIFY_DEDUP_HOURS = 12


# ========== 异常 ==========


class MailError(Exception):
    def __init__(self, code: str, msg: str) -> None:
        super().__init__(msg)
        self.code = code


# ========== 数据模型 ==========


@dataclass
class MessageRow:
    id: str
    from_user_id: str
    to_user_id: str
    body: str
    sent_at: int
    delivery_status: str
    delivery_failure_reason: str | None
    notify_attempted_at: int | None
    notify_status: str | None
    notify_error: str | None
    read_at: int | None
    replied_at: int | None
    reply_to_message_id: str | None
    deleted_by_recipient_at: int | None
    envelope_source: str
    expires_at: int


@dataclass
class EnvelopeState:
    free_remaining_today: int
    paid_remaining: int
    total_purchased: int


# ========== 主入口：发信 ==========


def send(from_user_id: str, to_user_id: str, body: str) -> MessageRow:
    """A 给 B 发信。

    投递前检查拉黑 / 对方存在；投递成功扣信封。失败不扣（钱退）。
    banned / frozen 用户直接拒绝发信（FEAT-025）。
    """
    if from_user_id == to_user_id:
        raise MailError("self_send", "不能给自己发信")

    # FEAT-019：业务层限流（mail_send_min=10/60s, mail_send_hour=100/3600s）
    from . import rate_limit as rl

    try:
        rl.check_group(from_user_id, "mail_send")
    except rl.RateLimitExceeded as e:
        raise MailError("rate_limited", str(e)) from e

    # FEAT-025：封号 / 冻结用户不能发信
    from . import admin as admin_svc

    try:
        admin_svc.assert_user_active(from_user_id)
    except admin_svc.AdminError as e:
        raise MailError(e.code, str(e)) from e

    # FEAT-034：未实名用户不能发信（PRD-001 决策点 #1）
    from . import realname as realname_svc

    try:
        realname_svc.assert_user_verified(from_user_id)
    except realname_svc.RealnameError as e:
        raise MailError(e.code, str(e)) from e

    body = _validate_body(body)
    now = now_ts()

    with get_conn() as conn:
        # 检查接收方存在（不存在直接抛，不落 messages 避 FK）
        to_row = conn.execute(
            "SELECT id FROM users WHERE id = ? AND deleted_at IS NULL",
            (to_user_id,),
        ).fetchone()
        if to_row is None:
            raise MailError("recipient_not_found", f"user {to_user_id} 不存在或已注销")

        delivery_status: str
        delivery_failure_reason: str | None = None
        envelope_source: str | None = None

        if _is_blocked_by(conn, from_user_id, to_user_id):
            delivery_status = "blocked_by_recipient"
            delivery_failure_reason = "对方已拉黑你"
        else:
            delivery_status = "delivered"

        # 投递成功才扣信封
        if delivery_status == "delivered":
            envelope_source = _consume_envelope(conn, from_user_id, now)

        msg_id = new_id()
        expires_at = now + MESSAGE_TTL_DAYS * 86400
        conn.execute(
            """
            INSERT INTO messages (
                id, from_user_id, to_user_id, body, sent_at,
                delivery_status, delivery_failure_reason, envelope_source, expires_at
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                msg_id,
                from_user_id,
                to_user_id,
                body,
                now,
                delivery_status,
                delivery_failure_reason,
                envelope_source or "free_daily",
                expires_at,
            ),
        )
        row = conn.execute("SELECT * FROM messages WHERE id = ?", (msg_id,)).fetchone()
        msg = _row_to_message(row)

    # 投递成功才触发通知（通知在事务外，不回滚扣信封）
    if msg.delivery_status == "delivered":
        _maybe_notify(msg)

    if msg.delivery_status != "delivered":
        # 拉黑失败暴露业务错误给 CLI
        raise MailError(msg.delivery_status, msg.delivery_failure_reason or "投递失败")

    return _reload_message(msg.id)


def _validate_body(body: str) -> str:
    if not isinstance(body, str):
        raise MailError("invalid_body", "body 必须是字符串")
    body = body.strip()
    if not body:
        raise MailError("invalid_body", "body 不能为空")
    if len(body) > BODY_MAX_LEN:
        raise MailError("invalid_body", f"body 长度 ≤ {BODY_MAX_LEN}")
    return body


# ========== 信封配额 ==========


def _day_key(ts: int) -> int:
    """UTC 日期整数 YYYYMMDD。"""
    d = _dt.datetime.fromtimestamp(ts, tz=_dt.UTC).date()
    return d.year * 10000 + d.month * 100 + d.day


def _consume_envelope(conn, user_id: str, now: int) -> str:
    """扣一个信封；返回 'free_daily' 或 'paid'。信封不足抛异常。"""
    day = _day_key(now)
    # 查今日免费使用量
    usage = conn.execute(
        "SELECT free_used, paid_used FROM envelope_usage WHERE user_id = ? AND day_key = ?",
        (user_id, day),
    ).fetchone()
    free_used = usage["free_used"] if usage else 0

    if free_used < FREE_ENVELOPES_PER_DAY:
        # 优先扣免费
        if usage is None:
            conn.execute(
                "INSERT INTO envelope_usage (user_id, day_key, free_used, paid_used) "
                "VALUES (?, ?, 1, 0)",
                (user_id, day),
            )
        else:
            conn.execute(
                "UPDATE envelope_usage SET free_used = free_used + 1 "
                "WHERE user_id = ? AND day_key = ?",
                (user_id, day),
            )
        return "free_daily"

    # 免费用完，扣付费
    balance = conn.execute(
        "SELECT paid_remaining FROM envelope_balance WHERE user_id = ?",
        (user_id,),
    ).fetchone()
    paid_remaining = balance["paid_remaining"] if balance else 0
    if paid_remaining <= 0:
        raise MailError(
            "insufficient_envelopes",
            f"今日免费额度（{FREE_ENVELOPES_PER_DAY}）已用完，付费信封余额 0。"
            "跑 `xq envelope buy --count N` 购买。",
        )
    conn.execute(
        "UPDATE envelope_balance SET paid_remaining = paid_remaining - 1 WHERE user_id = ?",
        (user_id,),
    )
    if usage is None:
        conn.execute(
            "INSERT INTO envelope_usage (user_id, day_key, free_used, paid_used) "
            "VALUES (?, ?, ?, 1)",
            (user_id, day, free_used),
        )
    else:
        conn.execute(
            "UPDATE envelope_usage SET paid_used = paid_used + 1 WHERE user_id = ? AND day_key = ?",
            (user_id, day),
        )
    return "paid"


def get_envelope_state(user_id: str) -> EnvelopeState:
    now = now_ts()
    day = _day_key(now)
    with get_conn() as conn:
        usage = conn.execute(
            "SELECT free_used FROM envelope_usage WHERE user_id = ? AND day_key = ?",
            (user_id, day),
        ).fetchone()
        free_used = usage["free_used"] if usage else 0
        balance = conn.execute(
            "SELECT paid_remaining, total_purchased FROM envelope_balance WHERE user_id = ?",
            (user_id,),
        ).fetchone()
        paid_remaining = balance["paid_remaining"] if balance else 0
        total_purchased = balance["total_purchased"] if balance else 0

    free_remaining = max(0, FREE_ENVELOPES_PER_DAY - free_used)
    return EnvelopeState(
        free_remaining_today=free_remaining,
        paid_remaining=paid_remaining,
        total_purchased=total_purchased,
    )


def buy_envelopes(user_id: str, count: int, mock: bool = False) -> dict[str, Any]:
    """买 N 个信封。

    mock=True：直接到账（测试 / --mock 用）。
    mock=False（默认）：调支付宝 precreate 生成二维码，订单状态 pending；
        用户扫码支付后支付宝异步 POST webhook，handle_alipay_webhook 里
        更新订单 + 充值余额。CLI 需要轮询 /envelope/state 或订单状态。
    """
    if count <= 0 or count > 10000:
        raise MailError("invalid_count", "count 必须 1-10000")

    now = now_ts()
    amount_cents = count * ENVELOPE_PRICE_CENTS
    order_id = new_id()

    if mock:
        with get_conn() as conn:
            conn.execute(
                """
                INSERT INTO envelope_orders (
                    id, user_id, count, amount_cents, mock, paid, created_at, provider, status
                )
                VALUES (?, ?, ?, ?, 1, 1, ?, 'mock', 'paid')
                """,
                (order_id, user_id, count, amount_cents, now),
            )
            _credit_balance(conn, user_id, count)
        return {
            "id": order_id,
            "count": count,
            "amount_cents": amount_cents,
            "mock": True,
            "status": "paid",
        }

    # 真付：先建 pending 订单，调 alipay precreate 拿 qr_code
    from . import alipay  # 延迟导入（cryptography 较重）

    with get_conn() as conn:
        conn.execute(
            """
            INSERT INTO envelope_orders (
                id, user_id, count, amount_cents, mock, paid, created_at, provider, status
            )
            VALUES (?, ?, ?, ?, 0, 0, ?, 'alipay', 'pending')
            """,
            (order_id, user_id, count, amount_cents, now),
        )

    subject = f"xiangqin 信封 x{count}"
    try:
        qr_code = alipay.create_precreate(order_id, amount_cents, subject)
    except alipay.AlipayError as e:
        # 失败订单标记 failed 便于对账
        with get_conn() as conn:
            conn.execute(
                "UPDATE envelope_orders SET status='failed' WHERE id=?",
                (order_id,),
            )
        raise MailError("payment_provider_error", str(e)) from e

    return {
        "id": order_id,
        "count": count,
        "amount_cents": amount_cents,
        "mock": False,
        "status": "pending",
        "qr_code": qr_code,  # 用户扫码支付
        "expires_in_s": 300,
    }


def _credit_balance(conn, user_id: str, count: int) -> None:
    """订单 paid 后充值余额。幂等调用方式由上层保证。"""
    balance = conn.execute(
        "SELECT paid_remaining FROM envelope_balance WHERE user_id = ?",
        (user_id,),
    ).fetchone()
    if balance is None:
        conn.execute(
            "INSERT INTO envelope_balance (user_id, paid_remaining, total_purchased) "
            "VALUES (?, ?, ?)",
            (user_id, count, count),
        )
    else:
        conn.execute(
            "UPDATE envelope_balance "
            "SET paid_remaining = paid_remaining + ?, total_purchased = total_purchased + ? "
            "WHERE user_id = ?",
            (count, count, user_id),
        )


def handle_alipay_webhook(body: bytes) -> dict[str, str]:
    """处理支付宝异步回调（POST /envelope/webhook/alipay）。

    - 验签
    - 找订单
    - 幂等：已 paid 订单不重复充值
    - 更新状态 + 充值余额
    """
    from . import alipay

    order_id, status = alipay.verify_webhook(body)

    with get_conn() as conn:
        row = conn.execute(
            "SELECT user_id, count, status, paid FROM envelope_orders WHERE id=?",
            (order_id,),
        ).fetchone()
        if row is None:
            raise MailError("order_not_found", f"订单不存在: {order_id}")

        user_id, count, prev_status, prev_paid = row

        # 幂等：已 paid 订单不重复充值
        if prev_paid == 1:
            return {"order_id": order_id, "status": prev_status, "action": "noop"}

        if status == "paid":
            conn.execute(
                "UPDATE envelope_orders SET status='paid', paid=1 WHERE id=?",
                (order_id,),
            )
            _credit_balance(conn, user_id, count)
            return {"order_id": order_id, "status": "paid", "action": "credited"}
        elif status in ("failed", "cancelled"):
            conn.execute(
                "UPDATE envelope_orders SET status=? WHERE id=?",
                (status, order_id),
            )
            return {"order_id": order_id, "status": status, "action": "marked"}
        else:
            # pending，不改状态
            return {"order_id": order_id, "status": prev_status, "action": "pending"}


def get_order(user_id: str, order_id: str) -> dict[str, Any] | None:
    """查订单状态（给 CLI 轮询用）。"""
    with get_conn() as conn:
        row = conn.execute(
            "SELECT id, user_id, count, amount_cents, status, paid, provider, created_at "
            "FROM envelope_orders WHERE id=?",
            (order_id,),
        ).fetchone()
    if row is None:
        return None
    if row[1] != user_id:
        raise MailError("forbidden", "不是你的订单")
    return {
        "id": row[0],
        "count": row[2],
        "amount_cents": row[3],
        "status": row[4],
        "paid": bool(row[5]),
        "provider": row[6],
        "created_at": row[7],
    }


# ========== 收发件查询 ==========


def list_sent(user_id: str, limit: int = 50) -> list[MessageRow]:
    limit = max(1, min(100, limit))
    with get_conn() as conn:
        rows = conn.execute(
            "SELECT * FROM messages WHERE from_user_id = ? ORDER BY sent_at DESC LIMIT ?",
            (user_id, limit),
        ).fetchall()
    return [_row_to_message(r) for r in rows]


def list_inbox(user_id: str, limit: int = 50) -> list[MessageRow]:
    """收件箱：只看 delivered 且未被自己删的，按时间倒序。"""
    limit = max(1, min(100, limit))
    now = now_ts()
    with get_conn() as conn:
        rows = conn.execute(
            """
            SELECT * FROM messages
            WHERE to_user_id = ?
              AND delivery_status = 'delivered'
              AND deleted_by_recipient_at IS NULL
              AND expires_at > ?
            ORDER BY sent_at DESC
            LIMIT ?
            """,
            (user_id, now, limit),
        ).fetchall()
        # 更新 last_read_inbox_at（标记 B 来查过 inbox；通知去重用）
        _upsert_notify_state(conn, user_id, last_read_at=now)
    return [_row_to_message(r) for r in rows]


def get_message(user_id: str, message_id: str, *, as_role: str = "any") -> MessageRow:
    """查单条。as_role = 'sender' / 'recipient' / 'any'，用于鉴权。"""
    with get_conn() as conn:
        row = conn.execute("SELECT * FROM messages WHERE id = ?", (message_id,)).fetchone()
    if row is None:
        raise MailError("message_not_found", f"message {message_id} 不存在")
    msg = _row_to_message(row)
    if as_role == "sender" and msg.from_user_id != user_id:
        raise MailError("forbidden", "非发件人")
    if as_role == "recipient" and msg.to_user_id != user_id:
        raise MailError("forbidden", "非收件人")
    if as_role == "any" and user_id not in (msg.from_user_id, msg.to_user_id):
        raise MailError("forbidden", "无权查看")
    return msg


def read_message(user_id: str, message_id: str) -> MessageRow:
    """标记已读（只有收件方能调用）。"""
    msg = get_message(user_id, message_id, as_role="recipient")
    if msg.read_at is not None:
        return msg
    now = now_ts()
    with get_conn() as conn:
        conn.execute(
            "UPDATE messages SET read_at = ? WHERE id = ? AND read_at IS NULL",
            (now, message_id),
        )
        _upsert_notify_state(conn, user_id, last_read_at=now)
    return _reload_message(message_id)


def delete_message_by_recipient(user_id: str, message_id: str) -> None:
    msg = get_message(user_id, message_id, as_role="recipient")
    if msg.deleted_by_recipient_at is not None:
        return
    now = now_ts()
    with get_conn() as conn:
        conn.execute(
            "UPDATE messages SET deleted_by_recipient_at = ? WHERE id = ?",
            (now, message_id),
        )


def reply(from_user_id: str, to_message_id: str, body: str) -> MessageRow:
    """回信：转发为新 message + 链回原信。原信标已回复。"""
    original = get_message(from_user_id, to_message_id, as_role="recipient")
    new_msg = send(from_user_id, original.from_user_id, body)
    now = now_ts()
    with get_conn() as conn:
        conn.execute(
            "UPDATE messages SET replied_at = ?, reply_to_message_id = ? WHERE id = ?",
            (now, new_msg.id, to_message_id),
        )
    return new_msg


# ========== 通知 ==========


def _maybe_notify(msg: MessageRow) -> None:
    """根据去重规则决定是否推送。"""
    from_user_id = msg.from_user_id
    to_user_id = msg.to_user_id

    with get_conn() as conn:
        # 查 B 的 profile 通知配置
        prof = conn.execute(
            "SELECT agent_gateway_url, agent_hooks_token, notify_on_new_mail "
            "FROM profiles WHERE user_id = ?",
            (to_user_id,),
        ).fetchone()

    status: str
    error: str | None = None
    attempted_at: int | None = None

    if (
        prof is None
        or not prof["agent_gateway_url"]
        or not prof["agent_hooks_token"]
        or not prof["notify_on_new_mail"]
    ):
        status = "not_enabled"
    elif _should_suppress_dup(to_user_id):
        status = "suppressed_dup"
    else:
        attempted_at = now_ts()
        status, error = _push_notification(
            prof["agent_gateway_url"],
            prof["agent_hooks_token"],
            from_user_id,
            msg.id,
        )
        if status == "pushed":
            _upsert_notify_state(None, to_user_id, last_notified_at=attempted_at)

    with get_conn() as conn:
        conn.execute(
            "UPDATE messages SET notify_status = ?, notify_attempted_at = ?, notify_error = ? "
            "WHERE id = ?",
            (status, attempted_at, error, msg.id),
        )


def _should_suppress_dup(user_id: str) -> bool:
    """有未读（且不是本次的），且距上次推送 < 12h → 抑制。"""
    now = now_ts()
    with get_conn() as conn:
        state = conn.execute(
            "SELECT last_notified_at FROM notify_state WHERE user_id = ?",
            (user_id,),
        ).fetchone()
        if state is None or state["last_notified_at"] is None:
            return False
        unread = conn.execute(
            "SELECT COUNT(*) AS n FROM messages "
            "WHERE to_user_id = ? AND delivery_status = 'delivered' "
            "AND deleted_by_recipient_at IS NULL AND read_at IS NULL",
            (user_id,),
        ).fetchone()
    # 注意：当前这封信已经落库，所以 unread.n >= 1。
    # >=2 才表示"除了这封外还有未读"
    if unread["n"] < 2:
        return False
    if now - state["last_notified_at"] < NOTIFY_DEDUP_HOURS * 3600:
        return True
    return False


PUSH_RETRY_MAX = 2  # 共 1 + PUSH_RETRY_MAX = 3 次尝试
PUSH_RETRY_BACKOFF_S = [2, 5]  # 第 1 次失败后等 2s / 第 2 次失败后等 5s


def retry_failed_notifications(limit: int = 50, since_hours: int = 24) -> dict[str, Any]:
    """FEAT-018 admin：扫 notify_status='push_failed' 的消息重试。

    返回统计 + 每条 message 的新 status。不抛异常；失败的消息状态更新为
    retry_last_error（若 push 成功则 'pushed'）。
    """
    cutoff = now_ts() - since_hours * 3600
    with get_conn() as conn:
        rows = conn.execute(
            "SELECT id, from_user_id, to_user_id FROM messages "
            "WHERE notify_status = 'push_failed' "
            "AND notify_attempted_at IS NOT NULL "
            "AND notify_attempted_at >= ? "
            "ORDER BY notify_attempted_at DESC LIMIT ?",
            (cutoff, limit),
        ).fetchall()

    scanned = len(rows)
    pushed = 0
    still_failed = 0
    suppressed = 0
    details: list[dict[str, Any]] = []

    # _maybe_notify 实际只用 msg.id/from_user_id/to_user_id 三个字段，
    # 用一个轻量 namedtuple 式 shim 避免构造完整 MessageRow（字段多且易错）
    from types import SimpleNamespace

    for row in rows:
        msg_shim = SimpleNamespace(
            id=row["id"],
            from_user_id=row["from_user_id"],
            to_user_id=row["to_user_id"],
        )
        _maybe_notify(msg_shim)  # type: ignore[arg-type]
        # 重读状态判断结果
        with get_conn() as conn:
            after = conn.execute(
                "SELECT notify_status, notify_error FROM messages WHERE id = ?",
                (row["id"],),
            ).fetchone()
        new_status = after["notify_status"] if after else "unknown"
        err = (after["notify_error"] if after else "") or ""
        if new_status == "pushed":
            pushed += 1
        elif new_status == "suppressed_dup":
            suppressed += 1
        else:
            still_failed += 1
        details.append({"message_id": row["id"], "status": new_status, "error": err})

    return {
        "scanned": scanned,
        "pushed": pushed,
        "still_failed": still_failed,
        "suppressed": suppressed,
        "details": details,
    }


def _push_notification(
    gateway_url: str,
    hooks_token: str,
    from_user_id: str,
    message_id: str,
) -> tuple[str, str | None]:
    """POST /hooks/agent。失败自动 retry 最多 PUSH_RETRY_MAX 次（exp backoff）。
    返回 (status, error)。"""
    url = gateway_url.rstrip("/") + "/hooks/agent"
    body = {
        "message": (
            f"你收到一封新 xiangqin 私信，来自 user_id {from_user_id}。"
            f"用 `xq inbox show {message_id}` 查看全文，"
            f'`xq inbox reply {message_id} "..."` 回复，'
            f"`xq inbox report {message_id}` 举报。"
        ),
        "name": "xiangqin",
        "deliver": True,
        "channel": "last",
    }
    headers = {"Authorization": f"Bearer {hooks_token}", "Content-Type": "application/json"}

    last_error: str | None = None
    attempts = 1 + PUSH_RETRY_MAX
    for attempt in range(attempts):
        try:
            r = httpx.post(url, json=body, headers=headers, timeout=10.0, trust_env=False)
            if r.status_code == 200:
                return "pushed", None
            last_error = f"HTTP {r.status_code}: {r.text[:200]}"
        except httpx.HTTPError as e:
            last_error = f"HTTP error: {e}"

        # 最后一次不 sleep（直接返回失败）
        if attempt < PUSH_RETRY_MAX:
            backoff = PUSH_RETRY_BACKOFF_S[attempt]
            time.sleep(backoff)

    return "push_failed", f"after {attempts} attempts: {last_error}"


def _upsert_notify_state(
    conn,
    user_id: str,
    *,
    last_notified_at: int | None = None,
    last_read_at: int | None = None,
) -> None:
    """更新通知状态。conn=None 时开新连接。"""
    close = False
    if conn is None:
        from contextlib import nullcontext  # noqa: F401

        close = True
        cm = get_conn()
        conn = cm.__enter__()  # type: ignore[attr-defined]
    try:
        row = conn.execute(
            "SELECT user_id FROM notify_state WHERE user_id = ?", (user_id,)
        ).fetchone()
        if row is None:
            conn.execute(
                "INSERT INTO notify_state (user_id, last_notified_at, last_read_inbox_at) "
                "VALUES (?, ?, ?)",
                (user_id, last_notified_at, last_read_at),
            )
        else:
            sets = []
            vals: list[Any] = []
            if last_notified_at is not None:
                sets.append("last_notified_at = ?")
                vals.append(last_notified_at)
            if last_read_at is not None:
                sets.append("last_read_inbox_at = ?")
                vals.append(last_read_at)
            if sets:
                vals.append(user_id)
                conn.execute(
                    f"UPDATE notify_state SET {', '.join(sets)} WHERE user_id = ?",
                    vals,
                )
    finally:
        if close:
            conn.__exit__(None, None, None)  # type: ignore[attr-defined]


# ========== 举报 / 拉黑 ==========


def report(reporter_id: str, message_id: str, reason: str | None = None) -> dict[str, Any]:
    msg = get_message(reporter_id, message_id, as_role="recipient")
    now = now_ts()
    report_id = new_id()
    with get_conn() as conn:
        conn.execute(
            "INSERT INTO reports (id, message_id, reporter_id, reported_id, reason, created_at) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            (report_id, message_id, reporter_id, msg.from_user_id, reason, now),
        )
    return {"id": report_id, "reported_user_id": msg.from_user_id, "created_at": now}


def block(blocker_id: str, blocked_id: str) -> None:
    if blocker_id == blocked_id:
        raise MailError("self_block", "不能拉黑自己")
    now = now_ts()
    with get_conn() as conn:
        conn.execute(
            "INSERT OR IGNORE INTO blocks (blocker_id, blocked_id, created_at) VALUES (?, ?, ?)",
            (blocker_id, blocked_id, now),
        )


def unblock(blocker_id: str, blocked_id: str) -> None:
    with get_conn() as conn:
        conn.execute(
            "DELETE FROM blocks WHERE blocker_id = ? AND blocked_id = ?",
            (blocker_id, blocked_id),
        )


def blocklist(user_id: str) -> list[dict[str, Any]]:
    with get_conn() as conn:
        rows = conn.execute(
            "SELECT blocked_id, created_at FROM blocks WHERE blocker_id = ? "
            "ORDER BY created_at DESC",
            (user_id,),
        ).fetchall()
    return [{"blocked_id": r["blocked_id"], "created_at": r["created_at"]} for r in rows]


def _is_blocked_by(conn, sender_id: str, recipient_id: str) -> bool:
    """recipient 是否已拉黑 sender。"""
    row = conn.execute(
        "SELECT 1 FROM blocks WHERE blocker_id = ? AND blocked_id = ?",
        (recipient_id, sender_id),
    ).fetchone()
    return row is not None


# ========== 内部工具 ==========


def _row_to_message(row) -> MessageRow:
    return MessageRow(
        id=row["id"],
        from_user_id=row["from_user_id"],
        to_user_id=row["to_user_id"],
        body=row["body"],
        sent_at=row["sent_at"],
        delivery_status=row["delivery_status"],
        delivery_failure_reason=row["delivery_failure_reason"],
        notify_attempted_at=row["notify_attempted_at"],
        notify_status=row["notify_status"],
        notify_error=row["notify_error"],
        read_at=row["read_at"],
        replied_at=row["replied_at"],
        reply_to_message_id=row["reply_to_message_id"],
        deleted_by_recipient_at=row["deleted_by_recipient_at"],
        envelope_source=row["envelope_source"],
        expires_at=row["expires_at"],
    )


def _reload_message(message_id: str) -> MessageRow:
    with get_conn() as conn:
        row = conn.execute("SELECT * FROM messages WHERE id = ?", (message_id,)).fetchone()
    return _row_to_message(row)
