"""Intercompany position aggregation: Decimal math, conservation and drill-down.

Every confirmed ledger event produces a signed claim: the payer books a debit
(claim ``+amount``) and the payee books a credit (claim ``-amount``). For one
company pair and currency the two perspectives must mirror exactly
(``C_A == -C_B``) — the module asserts that invariant after every pair
aggregation and refuses to render a non-conserving number. Subjects are
mirrored from the stored perspective; all money math uses ``Decimal`` on the
stored decimal strings, never SQLite ``SUM`` or floating point.
"""

from __future__ import annotations

import base64
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import json
import sqlite3

from .ledger_events import current_revision
from .manual_records import list_records
from .master_data import mask_account_number
from .subjects import MIRROR, SUBJECTS, mirror_subject, subject_label
from . import matching


class PositionError(ValueError):
    """Calculation failure: conservation violated or bad parameters."""


class PositionInputError(ValueError):
    """Invalid query parameters (mapped to HTTP 400)."""


SUBJECT_LABEL_MAP = {
    "receivable": "应收", "payable": "应付",
    "other_receivable": "其他应收", "other_payable": "其他应付",
}


def today_shanghai() -> str:
    return datetime.now(timezone(timedelta(hours=8))).date().isoformat()


def validate_window(from_: object, cutoff: object) -> tuple[str, str]:
    pattern = r"^\d{4}-\d{2}-\d{2}$"
    import re

    def check(value, label):
        text = str(value or "")
        if not text or not re.fullmatch(pattern, text):
            raise PositionInputError(f"{label}必须是 YYYY-MM-DD 格式。")
        try:
            datetime.strptime(text, "%Y-%m-%d")
        except ValueError:
            raise PositionInputError(f"{label}不是有效日期。") from None
        return text

    start = check(from_, "from") or "0001-01-01"
    end = check(cutoff, "cutoff")
    if start > end:
        raise PositionInputError("from 不能晚于 cutoff。")
    return start, end


def encode_cursor(parts: tuple[str, ...]) -> str:
    raw = "|".join(str(part) for part in parts)
    return base64.urlsafe_b64encode(raw.encode("utf-8")).decode("ascii")


def decode_cursor(cursor: str | None, parts: int) -> tuple[str, ...] | None:
    if not cursor:
        return None
    try:
        raw = base64.urlsafe_b64decode(cursor.encode("ascii")).decode("utf-8")
    except Exception:
        raise PositionInputError("分页游标无效。") from None
    values = tuple(raw.split("|"))
    if len(values) != parts:
        raise PositionInputError("分页游标无效。")
    return values


def _row(r) -> dict[str, object]:
    return {key: r[key] for key in r.keys()}


# ---------------------------------------------------------------------------
# Event loading
# ---------------------------------------------------------------------------

_EVENT_SELECT = """
    SELECT p.ledger_event_id, p.ledger_revision_id, p.effective_at, p.amount,
           p.amount_scale, p.currency, p.payer_company_id, p.payee_company_id,
           p.perspective_company_id, p.subject_code, p.source_kind,
           p.posting_kind, p.reverses_ledger_event_id, p.adjusts_ledger_event_id,
           p.source_id, p.evidence_count,
           cpayer.name AS payer_company_name, cpayee.name AS payee_company_name
    FROM eligible_position_events p
    JOIN companies cpayer ON cpayer.id = p.payer_company_id
    JOIN companies cpayee ON cpayee.id = p.payee_company_id
"""


def _event_filters(
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
    company_id: int | None = None,
    pair: tuple[int, int] | None = None,
    subject: str | None = None,
    posting_kind: str | None = None,
    source_kind: str | None = None,
    viewer_company_id: int | None = None,
    state: str | None = None,
) -> tuple[str, list[object]]:
    conditions = ["p.effective_at >= ?", "p.effective_at <= ?"]
    params: list[object] = [from_, cutoff + "T23:59:59"]
    if currency:
        conditions.append("p.currency = ?")
        params.append(currency)
    if company_id is not None or pair is not None:
        if pair is not None:
            a, b = int(pair[0]), int(pair[1])
            conditions.append(
                "(p.payer_company_id IN (?, ?) AND p.payee_company_id IN (?, ?))"
            )
            params.extend([a, b, a, b])
        else:
            conditions.append("(p.payer_company_id = ? OR p.payee_company_id = ?)")
            params.extend([company_id, company_id])
    if subject:
        if subject not in SUBJECTS:
            raise PositionInputError("科目筛选无效。")
        # The stored subject lives on one company's perspective; the viewer on
        # the other side sees its mirror. Match both so mirror events are never
        # dropped from the filter.
        conditions.append("(p.subject_code = ? OR p.subject_code = ?)")
        params.extend([subject, MIRROR[subject]])
    if posting_kind:
        conditions.append("p.posting_kind = ?")
        params.append(posting_kind)
    if source_kind:
        conditions.append("p.source_kind = ?")
        params.append(source_kind)
    if viewer_company_id is not None:
        conditions.append("(p.payer_company_id = ? OR p.payee_company_id = ?)")
        params.extend([viewer_company_id, viewer_company_id])
    if state is not None:
        if state not in ("confirmed", "pending_subject"):
            raise PositionInputError("无效的事件状态。")
        conditions.append("p.state = ?")
        params.append(state)
    return "WHERE " + " AND ".join(conditions), params


def load_events(
    connection: sqlite3.Connection,
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
    company_id: int | None = None,
    pair: tuple[int, int] | None = None,
    subject: str | None = None,
    posting_kind: str | None = None,
    source_kind: str | None = None,
    viewer_company_id: int | None = None,
) -> list[sqlite3.Row]:
    where, params = _event_filters(
        from_=from_, cutoff=cutoff, currency=currency, company_id=company_id,
        pair=pair, subject=subject, posting_kind=posting_kind,
        source_kind=source_kind, viewer_company_id=viewer_company_id,
    )
    return connection.execute(
        _EVENT_SELECT + where + " ORDER BY p.ledger_event_id", params
    ).fetchall()


def load_all_events(
    connection: sqlite3.Connection,
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
    company_id: int | None = None,
    pair: tuple[int, int] | None = None,
    subject: str | None = None,
    state: str | None = None,
    posting_kind: str | None = None,
    source_kind: str | None = None,
    viewer_company_id: int | None = None,
) -> list[sqlite3.Row]:
    """Load every current ledger revision (confirmed and pending_subject)."""
    where, params = _event_filters(
        from_=from_, cutoff=cutoff, currency=currency, company_id=company_id,
        pair=pair, subject=subject, posting_kind=posting_kind,
        source_kind=source_kind, viewer_company_id=viewer_company_id,
        state=state,
    )
    return connection.execute(
        _DETAIL_SELECT + where + " ORDER BY p.ledger_event_id", params
    ).fetchall()


def signed_amount(event: sqlite3.Row, company_id: int) -> Decimal:
    """+amount when the company is the payer, -amount when it is the payee."""
    value = Decimal(event["amount"])
    if int(event["payer_company_id"]) == int(company_id):
        return value
    return -value


def viewer_direction(event: sqlite3.Row, company_id: int) -> str:
    return "outgoing" if int(event["payer_company_id"]) == int(company_id) else "incoming"


def viewer_subject(event: sqlite3.Row, company_id: int) -> str | None:
    if event["perspective_company_id"] is None:
        return None
    if int(event["perspective_company_id"]) == int(company_id):
        return event["subject_code"]
    return mirror_subject(event["subject_code"])


def _subject_side(event: sqlite3.Row, company_id: int) -> tuple[str, str, str]:
    """``(subject_code, side)`` for ``company_id``: side is 'debit' or 'credit'."""
    perspective = int(event["perspective_company_id"])
    is_payer = int(event["payer_company_id"]) == int(company_id)
    if perspective == int(company_id):
        subject = event["subject_code"]
        side = "debit" if is_payer else "credit"
    else:
        subject = mirror_subject(event["subject_code"])
        side = "debit" if is_payer else "credit"
    return subject, side, perspective


def _balance_payload(
    connection: sqlite3.Connection,
    events: list[sqlite3.Row],
    *,
    company_id: int,
    from_: str,
    cutoff: str,
    currency: str,
) -> dict[str, object]:
    """One company/currency balance row; zero events still yields the row."""
    debit = credit = signed = Decimal("0")
    event_count = 0
    for event in events:
        if event["currency"] != currency:
            continue
        if int(event["payer_company_id"]) == int(company_id):
            debit += Decimal(event["amount"])
            signed += Decimal(event["amount"])
        else:
            credit += Decimal(event["amount"])
            signed -= Decimal(event["amount"])
        event_count += 1
    unresolved = unresolved_for_company(
        connection, company_id, cutoff, currency=currency
    )
    direction = "receivable" if signed > 0 else ("payable" if signed < 0 else None)
    return {
        "company_id": company_id,
        "company_name": _company_name(connection, company_id),
        "window": {"from": from_, "cutoff": cutoff, "cutoff_inclusive": True},
        "currency": currency,
        "opening": {"status": "unavailable", "amount": None},
        "period": {"debit": str(debit), "credit": str(credit)},
        "result": {
            "kind": "period_net_change",
            "signed_amount": str(signed),
            "direction": direction,
            "label": "期间净变动",
        },
        "unresolved": unresolved,
        "trace": {
            "event_count": event_count,
            "events_url": (
                f"/api/admin/intercompany/events?company_id={company_id}"
                f"&from={from_}&cutoff={cutoff}&currency={currency}"
            ),
        },
    }


def _company_name(connection: sqlite3.Connection, company_id: int) -> str:
    row = connection.execute(
        "SELECT name FROM companies WHERE id = ?", (company_id,)
    ).fetchone()
    return row["name"] if row is not None else ""


# ---------------------------------------------------------------------------
# Unresolved amounts
# ---------------------------------------------------------------------------


def _pending_subject_events(
    connection: sqlite3.Connection, cutoff: str
) -> list[sqlite3.Row]:
    return connection.execute(
        """
        SELECT r.ledger_event_id, r.effective_at, r.amount, r.currency,
               r.payer_company_id, r.payee_company_id
        FROM current_ledger_event_revisions c
        JOIN ledger_event_revisions r ON r.id = c.revision_id
        WHERE r.state = 'pending_subject' AND r.effective_at <= ?
        ORDER BY r.ledger_event_id
        """,
        (cutoff + "T23:59:59",),
    ).fetchall()


def _manual_pending(
    connection: sqlite3.Connection, cutoff: str
) -> list[sqlite3.Row]:
    return connection.execute(
        """
        SELECT m.id, m.company_id, m.counterparty_company_id, m.occurred_at,
               m.amount, m.currency
        FROM manual_records m
        JOIN current_manual_record_decisions c ON c.record_id = m.id
        JOIN manual_record_decisions d ON d.id = c.decision_id
        WHERE d.state = 'pending' AND m.occurred_at <= ?
        ORDER BY m.id
        """,
        (cutoff + "T23:59:59",),
    ).fetchall()


def _unmatched_singles(
    connection: sqlite3.Connection, company_id: int, cutoff: str
) -> list[sqlite3.Row]:
    return matching.unresolved_amounts(connection, company_id, cutoff + "T23:59:59")


def unresolved_for_company(
    connection: sqlite3.Connection,
    company_id: int,
    cutoff: str,
    currency: str | None = None,
    *,
    include_own_manual_only: bool = True,
    counterparty_filter: tuple[int, int] | None = None,
) -> dict[str, object]:
    """Unresolved absolute gross per currency (no plus/minus netting).

    ``by_reason`` buckets: subject_review (bank events waiting for a subject),
    unmatched_single (B-43 open observations) and manual_pending (unapproved
    manual records; only the submitting company / the admin may see them).
    """
    gross: dict[str, Decimal] = {}
    counts: dict[str, int] = {}
    reasons: dict[str, dict[str, object]] = {}

    def add(reason: str, cur: str, amount: Decimal) -> None:
        if currency and cur != currency:
            return
        if reason not in reasons:
            reasons[reason] = {"gross_amount": Decimal("0"), "count": 0}
        reasons[reason]["gross_amount"] += amount
        reasons[reason]["count"] += 1
        gross[cur] = gross.get(cur, Decimal("0")) + amount
        counts[cur] = counts.get(cur, 0) + 1

    for event in _pending_subject_events(connection, cutoff):
        if counterparty_filter is not None:
            a, b = counterparty_filter
            if not ({event["payer_company_id"], event["payee_company_id"]} == {a, b}):
                continue
        elif company_id not in (event["payer_company_id"], event["payee_company_id"]):
            continue
        add("subject_review", event["currency"], Decimal(event["amount"]))

    for row in _unmatched_singles(connection, company_id, cutoff):
        if counterparty_filter is not None:
            participants = _single_participants(connection, row["decision_id"])
            a, b = counterparty_filter
            if participants != {a, b}:
                continue
        add("unmatched_single", row["currency"], abs(Decimal(row["amount"])))

    for row in _manual_pending(connection, cutoff):
        if counterparty_filter is not None:
            a, b = counterparty_filter
            if not ({row["company_id"], row["counterparty_company_id"]} == {a, b}):
                continue
        elif include_own_manual_only and row["company_id"] != int(company_id):
            continue
        elif not include_own_manual_only and company_id not in (
            row["company_id"], row["counterparty_company_id"],
        ):
            continue
        add("manual_pending", row["currency"], Decimal(row["amount"]))

    items = sorted(gross.items())
    return {
        "gross_amount": str(sum((gross[k] for k, _ in items), Decimal("0"))),
        "count": sum(counts.values()),
        "by_reason": {
            reason: {
                "gross_amount": str(payload["gross_amount"]),
                "count": payload["count"],
            }
            for reason, payload in sorted(reasons.items())
        },
    }


def _single_participants(connection: sqlite3.Connection, decision_id: int) -> set[int]:
    rows = connection.execute(
        "SELECT company_id FROM transfer_decision_participants WHERE decision_id = ?",
        (decision_id,),
    ).fetchall()
    return {row["company_id"] for row in rows if row["company_id"] is not None}


# ---------------------------------------------------------------------------
# Directory
# ---------------------------------------------------------------------------


def company_balances(
    connection: sqlite3.Connection,
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
    company_id: int | None = None,
    limit: int = 50,
    cursor: str | None = None,
) -> dict[str, object]:
    from_, cutoff = validate_window(from_, cutoff)
    events = load_events(
        connection, from_=from_, cutoff=cutoff, currency=currency,
        company_id=company_id,
    )
    all_companies = _event_companies(connection)
    items_by_company: dict[int, list[sqlite3.Row]] = {}
    for event in events:
        for company in (event["payer_company_id"], event["payee_company_id"]):
            items_by_company.setdefault(company, []).append(event)

    rows: list[tuple[int, str]] = []
    for company in all_companies:
        if company_id is not None and company != company_id:
            continue
        if company in items_by_company:
            rows.append((company, "has_events"))
        else:
            # Companies with no confirmed events still appear when they have
            # unresolved exposure, so the directory never hides risk.
            unresolved = unresolved_for_company(connection, company, cutoff, currency)
            if unresolved["count"] > 0:
                rows.append((company, "unresolved"))

    if cursor is not None:
        decoded = decode_cursor(cursor, 2)
        cursor_company = int(decoded[0])
        cursor_currency = decoded[1]
    else:
        cursor_company, cursor_currency = None, None

    flat: list[tuple[int, str]] = []
    pending_subject = _pending_subject_events(connection, cutoff)
    for company, _marker in rows:
        bucket_events = items_by_company.get(company, [])
        pending_currencies = {
            event["currency"]
            for event in pending_subject
            if company in (event["payer_company_id"], event["payee_company_id"])
        }
        bucket_currencies = sorted(
            {event["currency"] for event in bucket_events}
            | pending_currencies
            | {
                item["currency"]
                for item in _manual_pending(connection, cutoff)
                if item["company_id"] == company or item["counterparty_company_id"] == company
            }
            | {
                item["currency"] for item in _unmatched_singles(connection, company, cutoff)
            }
        )
        if currency:
            bucket_currencies = [currency] if currency in bucket_currencies else []
        for cur in bucket_currencies:
            flat.append((company, cur))

    filtered: list[tuple[int, str]] = []
    for company, cur in flat:
        if cursor_company is not None:
            if company < cursor_company:
                continue
            if company == cursor_company and cur <= cursor_currency:
                continue
        filtered.append((company, cur))

    filtered.sort(key=lambda item: (item[0], item[1]))
    page = filtered[:limit]
    has_more = len(filtered) > limit
    next_cursor = None
    if has_more:
        last = page[-1]
        next_cursor = encode_cursor((str(last[0]), last[1]))

    items = []
    for company, cur in page:
        items.append(
            _balance_payload(
                connection, items_by_company.get(company, []),
                company_id=company, from_=from_, cutoff=cutoff, currency=cur,
            )
        )
    return {
        "window": {"from": from_, "cutoff": cutoff, "cutoff_inclusive": True},
        "items": items,
        "next_cursor": next_cursor,
        "has_more": has_more,
    }


def _event_companies(connection: sqlite3.Connection) -> list[int]:
    """All master companies (plus any with ledger exposure) for the directory."""
    rows = connection.execute(
        """
        SELECT id AS company_id FROM companies
        UNION
        SELECT payer_company_id AS company_id FROM eligible_position_events
        UNION
        SELECT payee_company_id AS company_id FROM eligible_position_events
        UNION
        SELECT payer_company_id AS company_id FROM ledger_event_revisions
        WHERE state = 'pending_subject'
        UNION
        SELECT payee_company_id AS company_id FROM ledger_event_revisions
        WHERE state = 'pending_subject'
        ORDER BY company_id
        """
    ).fetchall()
    return [row["company_id"] for row in rows]


# ---------------------------------------------------------------------------
# Pair detail
# ---------------------------------------------------------------------------


def pair_detail(
    connection: sqlite3.Connection,
    company_a: int,
    company_b: int,
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
) -> dict[str, object]:
    from_, cutoff = validate_window(from_, cutoff)
    if int(company_a) == int(company_b):
        raise PositionInputError("公司对的两个公司不能相同。")
    pair = (int(company_a), int(company_b))
    events = load_events(
        connection, from_=from_, cutoff=cutoff, currency=currency, pair=pair
    )
    pending_currencies = {
        event["currency"]
        for event in _pending_subject_events(connection, cutoff)
        if {event["payer_company_id"], event["payee_company_id"]} == set(pair)
    }
    manual_currencies = {
        row["currency"]
        for row in _manual_pending(connection, cutoff)
        if {row["company_id"], row["counterparty_company_id"]} == set(pair)
    }
    currencies = sorted(
        {event["currency"] for event in events} | pending_currencies | manual_currencies
    )
    if currency:
        currencies = [currency] if currency in currencies else []

    outputs = []
    for cur in currencies:
        subset = [event for event in events if event["currency"] == cur]
        outputs.append(
            _pair_currency(
                connection, pair, subset, from_=from_, cutoff=cutoff, currency=cur
            )
        )
    if not outputs:
        outputs.append(
            _pair_currency(
                connection, pair, [], from_=from_, cutoff=cutoff,
                currency=currency or "CNY",
            )
        )
    return {
        "window": {"from": from_, "cutoff": cutoff, "cutoff_inclusive": True},
        "companies": {
            "a": {"company_id": pair[0], "name": _company_name(connection, pair[0])},
            "b": {"company_id": pair[1], "name": _company_name(connection, pair[1])},
        },
        "items": outputs,
    }


def _pair_currency(
    connection: sqlite3.Connection,
    pair: tuple[int, int],
    events: list[sqlite3.Row],
    *,
    from_: str,
    cutoff: str,
    currency: str,
) -> dict[str, object]:
    a, b = pair
    debit_a = credit_a = debit_b = credit_b = Decimal("0")
    subject_sides: dict[str, dict[str, object]] = {}
    for subject in SUBJECTS:
        subject_sides[subject] = {
            "subject_code": subject,
            "label": SUBJECT_LABEL_MAP[subject],
            "a_debit": Decimal("0"), "a_credit": Decimal("0"),
            "b_debit": Decimal("0"), "b_credit": Decimal("0"),
            "count": 0,
        }
    for event in events:
        amount = Decimal(event["amount"])
        if int(event["payer_company_id"]) == a:
            debit_a += amount
            credit_b += amount
        else:
            debit_b += amount
            credit_a += amount
        for company in (a, b):
            subject, side, _perspective = _subject_side(event, company)
            bucket = subject_sides[subject]
            bucket[f"{'a' if company == a else 'b'}_{side}"] += amount
            bucket["count"] += 1

    signed_a = debit_a - credit_a
    signed_b = debit_b - credit_b
    if signed_a != -signed_b:
        raise PositionError(
            "公司对余额不守恒：A 与 B 的净结果未镜像，拒绝展示该数据。"
        )

    def result_of(signed: Decimal) -> dict[str, object]:
        direction = "receivable" if signed > 0 else ("payable" if signed < 0 else None)
        return {
            "kind": "period_net_change",
            "signed_amount": str(signed),
            "direction": direction,
            "label": "期间净变动",
        }

    exception_subjects = [
        subject for subject, bucket in subject_sides.items()
        if _subject_abnormal(bucket["a_debit"], bucket["a_credit"], subject)
        or _subject_abnormal(bucket["b_debit"], bucket["b_credit"], subject)
    ]
    return {
        "currency": currency,
        "opening": {"status": "unavailable", "amount": None},
        "a": {
            "period": {"debit": str(debit_a), "credit": str(credit_a)},
            "result": result_of(signed_a),
        },
        "b": {
            "period": {"debit": str(debit_b), "credit": str(credit_b)},
            "result": result_of(signed_b),
        },
        "conservation": {
            "abs_equal": abs(signed_a) == abs(signed_b),
            "opposite": signed_a == -signed_b,
        },
        "subjects": {
            subject: {
                "subject_code": bucket["subject_code"],
                "label": bucket["label"],
                "a_debit": str(bucket["a_debit"]),
                "a_credit": str(bucket["a_credit"]),
                "b_debit": str(bucket["b_debit"]),
                "b_credit": str(bucket["b_credit"]),
                "count": bucket["count"],
            }
            for subject, bucket in subject_sides.items()
            if bucket["count"] > 0
        },
        "normal_balance_exception": exception_subjects,
        "unresolved": unresolved_for_company(
            connection, a, cutoff, currency=currency,
            counterparty_filter=pair,
        ),
        "trace": {
            "event_count": len(events),
            "events_url": (
                f"/api/admin/intercompany/events?company_a={a}&company_b={b}"
                f"&from={from_}&cutoff={cutoff}&currency={currency}"
            ),
        },
    }


def _subject_abnormal(debit: Decimal, credit: Decimal, subject: str) -> bool:
    if subject in ("receivable", "other_receivable"):
        return debit < credit
    return credit < debit


# ---------------------------------------------------------------------------
# Events list and detail
# ---------------------------------------------------------------------------


def list_events(
    connection: sqlite3.Connection,
    *,
    from_: str,
    cutoff: str,
    currency: str | None = None,
    company_id: int | None = None,
    company_a: int | None = None,
    company_b: int | None = None,
    subject: str | None = None,
    state: str | None = None,
    posting_kind: str | None = None,
    source_kind: str | None = None,
    viewer_company_id: int | None = None,
    limit: int = 50,
    cursor: str | None = None,
) -> dict[str, object]:
    from_, cutoff = validate_window(from_, cutoff)
    pair = None
    if company_a is not None and company_b is not None:
        pair = (int(company_a), int(company_b))
    elif company_a is not None:
        company_id = int(company_a)
    events = load_all_events(
        connection, from_=from_, cutoff=cutoff, currency=currency,
        company_id=company_id, pair=pair, subject=subject, state=state,
        posting_kind=posting_kind, source_kind=source_kind,
        viewer_company_id=viewer_company_id,
    )

    def sort_key(event):
        return (event["effective_at"], event["ledger_event_id"])

    events.sort(key=sort_key, reverse=True)

    if cursor is not None:
        decoded = decode_cursor(cursor, 2)
        cursor_date = decoded[0]
        cursor_id = int(decoded[1])
    else:
        cursor_date, cursor_id = None, None
    filtered = []
    for event in events:
        if cursor_date is not None:
            key = sort_key(event)
            if key[0] > cursor_date or (key[0] == cursor_date and key[1] >= cursor_id):
                continue
        filtered.append(event)

    page = filtered[:limit]
    has_more = len(filtered) > limit
    next_cursor = None
    if has_more:
        last = page[-1]
        next_cursor = encode_cursor((last["effective_at"], str(last["ledger_event_id"])))

    items = []
    for event in page:
        item = event_payload(
            connection, event, viewer_company_id=viewer_company_id
        )
        item["state"] = event["state"]
        items.append(item)
    return {
        "window": {"from": from_, "cutoff": cutoff, "cutoff_inclusive": True},
        "items": items,
        "next_cursor": next_cursor,
        "has_more": has_more,
    }


def _current_state(connection: sqlite3.Connection, ledger_event_id: int) -> str:
    revision = current_revision(connection, ledger_event_id)
    return revision["state"] if revision is not None else ""


def event_payload(
    connection: sqlite3.Connection, event: sqlite3.Row, *, viewer_company_id: int | None = None
) -> dict[str, object]:
    item = {
        "ledger_event_id": event["ledger_event_id"],
        "ledger_revision_id": event["ledger_revision_id"],
        "effective_at": event["effective_at"],
        "amount": event["amount"],
        "currency": event["currency"],
        "payer_company_id": event["payer_company_id"],
        "payer_company_name": event["payer_company_name"],
        "payee_company_id": event["payee_company_id"],
        "payee_company_name": event["payee_company_name"],
        "perspective_company_id": event["perspective_company_id"],
        "subject_code": event["subject_code"],
        "subject_label": subject_label(event["subject_code"]),
        "posting_kind": event["posting_kind"],
        "source_kind": event["source_kind"],
        "reverses_ledger_event_id": event["reverses_ledger_event_id"],
        "adjusts_ledger_event_id": event["adjusts_ledger_event_id"],
        "source_id": event["source_id"],
        "evidence_count": event["evidence_count"],
    }
    if viewer_company_id is not None:
        item["direction"] = viewer_direction(event, viewer_company_id)
        own_subject = viewer_subject(event, viewer_company_id)
        item["own_subject"] = own_subject
        item["own_subject_label"] = (
            subject_label(own_subject) if own_subject is not None else None
        )
        item["counterparty_company_id"] = (
            event["payee_company_id"]
            if int(event["payer_company_id"]) == int(viewer_company_id)
            else event["payer_company_id"]
        )
        item["counterparty_company_name"] = (
            event["payee_company_name"]
            if int(event["payer_company_id"]) == int(viewer_company_id)
            else event["payer_company_name"]
        )
    item.update(_event_line_display(connection, event, viewer_company_id))
    return item


_REPAY_MARKERS = ("还款", "归还借款", "归还往来款")


def bank_short_name(name: str | None) -> str:
    text = str(name or "").strip()
    if text.startswith("中国"):
        text = text[2:]
    if text.endswith("银行"):
        text = text[:-2]
    return text or "银行"


def _account_chip(visibility: str, bank_name: str | None, account: str | None) -> dict[str, object]:
    if visibility == "missing":
        return {"visibility": "missing", "label": None}
    if visibility == "masked":
        return {"visibility": "masked", "label": "按对方授权不可见"}
    number = str(account or "")
    tail = number[-4:] if number else ""
    short = bank_short_name(bank_name)
    label = f"{short} {tail}".strip() if tail else short
    return {"visibility": "visible", "label": label}


def _event_line_display(
    connection: sqlite3.Connection,
    event: sqlite3.Row,
    viewer_company_id: int | None,
) -> dict[str, object]:
    """Account chips, summary and repayment flag for the event table."""
    missing = _account_chip("missing", None, None)
    payer_chip, payee_chip = missing, missing
    summary = None
    texts: list[str] = []
    sides = _event_source_sides(connection, event["ledger_event_id"])
    for side in sides:
        company_id = side["company_id"]
        own = viewer_company_id is None or int(company_id) == int(viewer_company_id)
        visibility = "visible" if own else "masked"
        chip = _account_chip(visibility, side.get("bank_name"), side.get("own_account"))
        if int(company_id) == int(event["payer_company_id"]):
            payer_chip = chip
        elif int(company_id) == int(event["payee_company_id"]):
            payee_chip = chip
        if own or viewer_company_id is None:
            if side.get("summary"):
                texts.append(str(side["summary"]))
            if side.get("reason"):
                texts.append(str(side["reason"]))
    if texts:
        summary = texts[0]
    blob = " ".join(texts)
    is_repayment = any(marker in blob for marker in _REPAY_MARKERS)
    return {
        "payer_account": payer_chip,
        "payee_account": payee_chip,
        "summary": summary,
        "is_repayment": is_repayment,
    }


def _event_source_sides(
    connection: sqlite3.Connection, ledger_event_id: int
) -> list[dict[str, object]]:
    rows = connection.execute(
        """
        SELECT b.company_id AS company_id, s.bank_name AS bank_name,
               r.own_account AS own_account, r.summary AS summary,
               r.purpose AS purpose, NULL AS reason
        FROM ledger_event_bank_sources bs
        JOIN current_transfer_decisions cur ON cur.event_id = bs.bank_event_id
        JOIN transfer_decision_observations o ON o.decision_id = cur.decision_id
        JOIN source_rows r ON r.id = o.source_row_id
        JOIN sheet_batches s ON s.id = r.sheet_batch_id
        JOIN import_batches b ON b.id = s.import_batch_id
        WHERE bs.ledger_event_id = ?
        """,
        (ledger_event_id,),
    ).fetchall()
    if rows:
        return [_row(row) for row in rows]
    manuals = connection.execute(
        """
        SELECT m.company_id AS company_id, ba.bank_name AS bank_name,
               ba.account_number AS own_account, m.summary AS summary,
               NULL AS purpose, m.reason AS reason
        FROM ledger_event_manual_sources ms
        JOIN manual_records m ON m.id = ms.manual_record_id
        LEFT JOIN bank_accounts ba ON ba.id = m.bank_account_id
        WHERE ms.ledger_event_id = ?
        """,
        (ledger_event_id,),
    ).fetchall()
    return [_row(row) for row in manuals]


_DETAIL_SELECT = """
    SELECT p.ledger_event_id, p.id AS ledger_revision_id, p.effective_at, p.amount,
           p.amount_scale, p.currency, p.payer_company_id, p.payee_company_id,
           p.perspective_company_id, p.subject_code, p.source_kind,
           p.posting_kind, p.reverses_ledger_event_id, p.adjusts_ledger_event_id,
           p.state, p.revision AS revision_number,
           cpayer.name AS payer_company_name, cpayee.name AS payee_company_name,
           COALESCE(
               (SELECT bs.bank_event_id FROM ledger_event_bank_sources bs
                WHERE bs.ledger_event_id = p.ledger_event_id LIMIT 1),
               (SELECT ms.manual_record_id FROM ledger_event_manual_sources ms
                WHERE ms.ledger_event_id = p.ledger_event_id LIMIT 1)
           ) AS source_id,
           ((SELECT COUNT(*) FROM ledger_event_bank_sources bs
             WHERE bs.ledger_event_id = p.ledger_event_id)
          + (SELECT COUNT(*) FROM ledger_event_manual_sources ms
             WHERE ms.ledger_event_id = p.ledger_event_id)) AS evidence_count
    FROM current_ledger_event_revisions cur
    JOIN ledger_event_revisions p ON p.id = cur.revision_id
    JOIN companies cpayer ON cpayer.id = p.payer_company_id
    JOIN companies cpayee ON cpayee.id = p.payee_company_id
"""


def event_detail(connection: sqlite3.Connection, ledger_event_id: int) -> dict[str, object] | None:
    event = connection.execute(
        _DETAIL_SELECT + " WHERE p.ledger_event_id = ?", (ledger_event_id,)
    ).fetchone()
    if event is None:
        return None
    history = connection.execute(
        """
        SELECT r.* FROM ledger_event_revisions r
        WHERE r.ledger_event_id = ?
        ORDER BY r.revision
        """,
        (ledger_event_id,),
    ).fetchall()
    suggestions = connection.execute(
        """
        SELECT s.*, c.name AS company_name
        FROM ledger_subject_suggestions s
        LEFT JOIN companies c ON c.id = s.suggested_perspective_company_id
        WHERE s.ledger_event_id = ?
        ORDER BY s.id
        """,
        (ledger_event_id,),
    ).fetchall()
    return {
        "event": event_payload(connection, event),
        "state": _current_state(connection, ledger_event_id),
        "history": [
            {
                "revision": rev["revision"],
                "state": rev["state"],
                "effective_at": rev["effective_at"],
                "amount": rev["amount"],
                "currency": rev["currency"],
                "perspective_company_id": rev["perspective_company_id"],
                "subject_code": rev["subject_code"],
                "posting_kind": rev["posting_kind"],
                "source_kind": rev["source_kind"],
                "source_revision_token": rev["source_revision_token"],
                "actor_username": rev["actor_username"],
                "reason": rev["reason"],
                "created_at": rev["created_at"],
            }
            for rev in history
        ],
        "suggestions": [
            {
                "suggested_perspective_company_id": sug["suggested_perspective_company_id"],
                "suggested_company_name": sug["company_name"],
                "suggested_subject_code": sug["suggested_subject_code"],
                "suggested_subject_label": subject_label(sug["suggested_subject_code"]),
                "rule_version": sug["rule_version"],
                "evidence": json.loads(sug["evidence_json"]) if sug["evidence_json"] else {},
                "created_at": sug["created_at"],
            }
            for sug in suggestions
        ],
    }


def subject_review_queue(
    connection: sqlite3.Connection,
    *,
    from_: str,
    cutoff: str,
    company_id: int | None = None,
    limit: int = 50,
    cursor: str | None = None,
) -> dict[str, object]:
    from_, cutoff = validate_window(from_, cutoff)
    conditions = ["r.state = 'pending_subject'", "r.effective_at >= ?", "r.effective_at <= ?"]
    params: list[object] = [from_, cutoff + "T23:59:59"]
    if company_id is not None:
        conditions.append("(r.payer_company_id = ? OR r.payee_company_id = ?)")
        params.extend([company_id, company_id])
    rows = connection.execute(
        f"""
        SELECT r.ledger_event_id, r.effective_at, r.amount, r.currency,
               r.payer_company_id, r.payee_company_id,
               cpayer.name AS payer_company_name, cpayee.name AS payee_company_name,
               r.id AS revision_id, r.evidence_json AS evidence_json
        FROM current_ledger_event_revisions cur
        JOIN ledger_event_revisions r ON r.id = cur.revision_id
        JOIN companies cpayer ON cpayer.id = r.payer_company_id
        JOIN companies cpayee ON cpayee.id = r.payee_company_id
        WHERE {' AND '.join(conditions)}
        ORDER BY r.ledger_event_id
        """,
        params,
    ).fetchall()
    rows.sort(key=lambda row: (row["effective_at"], row["ledger_event_id"]), reverse=True)
    if cursor is not None:
        decoded = decode_cursor(cursor, 2)
        cursor_date, cursor_id = decoded[0], int(decoded[1])
    else:
        cursor_date, cursor_id = None, None
    filtered = []
    for row in rows:
        evidence = json.loads(row["evidence_json"] or "{}") if row["evidence_json"] else {}
        if evidence.get("admin_disposition") == "exception":
            continue
        if cursor_date is not None:
            if row["effective_at"] > cursor_date or (
                row["effective_at"] == cursor_date and row["ledger_event_id"] >= cursor_id
            ):
                continue
        filtered.append(row)
    page = filtered[:limit]
    has_more = len(filtered) > limit
    next_cursor = None
    if has_more:
        last = page[-1]
        next_cursor = encode_cursor((last["effective_at"], str(last["ledger_event_id"])))
    items = []
    for row in page:
        suggestions = connection.execute(
            """
            SELECT s.suggested_perspective_company_id, s.suggested_subject_code,
                   c.name AS company_name, s.rule_version, s.evidence_json
            FROM ledger_subject_suggestions s
            LEFT JOIN companies c ON c.id = s.suggested_perspective_company_id
            WHERE s.ledger_event_id = ?
            ORDER BY s.id
            """,
            (row["ledger_event_id"],),
        ).fetchall()
        display = _event_line_display(connection, row, None)
        items.append(
            {
                "ledger_event_id": row["ledger_event_id"],
                "revision_id": row["revision_id"],
                "effective_at": row["effective_at"],
                "amount": row["amount"],
                "currency": row["currency"],
                "payer_company_id": row["payer_company_id"],
                "payer_company_name": row["payer_company_name"],
                "payee_company_id": row["payee_company_id"],
                "payee_company_name": row["payee_company_name"],
                "summary": display.get("summary"),
                "is_repayment": display.get("is_repayment"),
                "suggestions": [
                    {
                        "suggested_perspective_company_id": sug["suggested_perspective_company_id"],
                        "suggested_company_name": sug["company_name"],
                        "suggested_subject_code": sug["suggested_subject_code"],
                        "suggested_subject_label": subject_label(sug["suggested_subject_code"]),
                        "rule_version": sug["rule_version"],
                        "evidence": json.loads(sug["evidence_json"]) if sug["evidence_json"] else {},
                    }
                    for sug in suggestions
                ],
            }
        )
    return {
        "window": {"from": from_, "cutoff": cutoff, "cutoff_inclusive": True},
        "items": items,
        "next_cursor": next_cursor,
        "has_more": has_more,
    }


# ---------------------------------------------------------------------------
# Evidence drill-down
# ---------------------------------------------------------------------------


def event_evidence(
    connection: sqlite3.Connection,
    ledger_event_id: int,
    *,
    viewer_company_id: int | None = None,
) -> dict[str, object] | None:
    """Evidence blocks behind one ledger event with explicit visibility.

    ``visibility`` is always present (``visible``/``masked``/``missing``); a
    company user never guesses from absent fields. Company viewers see their
    own bank/manual evidence in full, the counterparty side masked, and a
    ``missing`` block when no source exists.
    """
    event = connection.execute(
        _EVENT_SELECT + " WHERE p.ledger_event_id = ?", (ledger_event_id,)
    ).fetchone()
    if event is None:
        return None
    blocks: list[dict[str, object]] = []
    bank_claims = connection.execute(
        "SELECT * FROM ledger_event_bank_sources WHERE ledger_event_id = ? ORDER BY bank_event_id",
        (ledger_event_id,),
    ).fetchall()
    manual_claims = connection.execute(
        "SELECT * FROM ledger_event_manual_sources WHERE ledger_event_id = ? ORDER BY manual_record_id",
        (ledger_event_id,),
    ).fetchall()

    for claim in bank_claims:
        blocks.extend(
            _bank_evidence_blocks(connection, claim["bank_event_id"], viewer_company_id)
        )
    for claim in manual_claims:
        blocks.append(
            _manual_evidence_block(connection, claim["manual_record_id"], viewer_company_id)
        )

    if not blocks:
        blocks.append(
            {
                "side": "counterparty",
                "source_kind": "bank",
                "visibility": "missing",
                "fields": {},
            }
        )

    return {
        "ledger_event_id": ledger_event_id,
        "summary": {
            "effective_at": event["effective_at"],
            "amount": event["amount"],
            "currency": event["currency"],
            "payer_company_id": event["payer_company_id"],
            "payer_company_name": event["payer_company_name"],
            "payee_company_id": event["payee_company_id"],
            "payee_company_name": event["payee_company_name"],
            "subject_code": event["subject_code"],
            "subject_label": subject_label(event["subject_code"]),
            "posting_kind": event["posting_kind"],
        },
        "blocks": blocks,
    }


def _bank_evidence_blocks(
    connection: sqlite3.Connection,
    bank_event_id: int,
    viewer_company_id: int | None,
) -> list[dict[str, object]]:
    decision = matching._current_decision_for_event(connection, bank_event_id)
    if decision is None:
        return []
    observations = matching._decision_observations(connection, decision["id"])
    blocks: list[dict[str, object]] = []
    for observation in observations:
        source = connection.execute(
            """
            SELECT r.id, r.source_row, r.transaction_at, r.income, r.expense,
                   r.own_account, r.own_name, r.counterparty_account,
                   r.counterparty_name, r.summary, r.purpose, r.reference,
                   r.currency, s.sheet_name, f.original_filename,
                   b.company_id AS batch_company_id, c.name AS company_name
            FROM source_rows r
            JOIN sheet_batches s ON s.id = r.sheet_batch_id
            JOIN import_batches b ON b.id = s.import_batch_id
            JOIN source_files f ON f.id = b.source_file_id
            LEFT JOIN companies c ON c.id = b.company_id
            WHERE r.id = ?
            """,
            (observation["source_row_id"],),
        ).fetchone()
        if source is None:
            continue
        own = int(source["batch_company_id"]) == int(viewer_company_id) if viewer_company_id is not None else True
        side = "own" if own else "counterparty"
        if viewer_company_id is None or own:
            visibility = "visible"
            fields = {
                "original_filename": source["original_filename"],
                "sheet_name": source["sheet_name"],
                "source_row": source["source_row"],
                "transaction_at": source["transaction_at"],
                "income": source["income"],
                "expense": source["expense"],
                "own_account": source["own_account"] if viewer_company_id is None
                else mask_account_number(source["own_account"]) if source["own_account"] else None,
                "own_name": source["own_name"],
                "counterparty_account_masked": (
                    mask_account_number(source["counterparty_account"])
                    if source["counterparty_account"] else None
                ),
                "counterparty_name": source["counterparty_name"],
                "summary": source["summary"],
                "purpose": source["purpose"],
                "reference": source["reference"],
                "currency": source["currency"],
                "role": observation["role"],
            }
        else:
            visibility = "masked"
            fields = {
                "company_id": source["batch_company_id"],
                "company_name": source["company_name"],
                "own_account_masked": (
                    mask_account_number(source["own_account"])
                    if source["own_account"] else None
                ),
                "note": "按对方授权不可见",
            }
        blocks.append(
            {
                "side": side,
                "source_kind": "bank",
                "visibility": visibility,
                "fields": fields,
            }
        )
    return blocks


def _manual_evidence_block(
    connection: sqlite3.Connection,
    manual_record_id: int,
    viewer_company_id: int | None,
) -> dict[str, object]:
    rows = list_records(connection, limit=10000)
    record = next((row for row in rows if row["id"] == manual_record_id), None)
    if record is None:
        return {
            "side": "counterparty", "source_kind": "manual",
            "visibility": "missing", "fields": {},
        }
    own = (
        int(record["company_id"]) == int(viewer_company_id)
        if viewer_company_id is not None
        else True
    )
    if viewer_company_id is None or own:
        visibility = "visible"
        fields = {
            "manual_record_id": record["id"],
            "company_id": record["company_id"],
            "company_name": record["company_name"],
            "counterparty_company_id": record["counterparty_company_id"],
            "counterparty_company_name": record["counterparty_company_name"],
            "occurred_at": record["occurred_at"],
            "direction": record["direction"],
            "amount": record["amount"],
            "currency": record["currency"],
            "funding_source": record["funding_source"],
            "requested_subject": record["requested_subject"],
            "summary": record["summary"],
            "reason": record["reason"],
            "state": record["state"],
        }
    else:
        visibility = "masked"
        fields = {
            "company_id": record["company_id"],
            "company_name": record["company_name"],
            "state": record["state"],
            "note": "按对方授权不可见",
        }
    return {
        "side": "own" if own else "counterparty",
        "source_kind": "manual",
        "visibility": visibility,
        "fields": fields,
    }
