"""The edge's MCP tool surface — read-only, structure not documents.

Transport-agnostic on purpose: this builds the tool manifest and dispatches calls, and a
caller wires it to stdio, HTTP or anything else. That keeps the module free of a protocol
dependency (the core declares zero dependencies and this does not change that) and lets the
same surface be served by the container, by a local host, by the local UI, or by a test.

Every tool is READ. There is no tool that records, seals or sends — an MCP host is a reader
of the company's own book, never a writer into it. A write surface would put the company's AI
inside the record path, which is exactly where `Application != Ledger Core` says it must not be.
"""

from __future__ import annotations

from typing import Any, Callable

TOOLS: list[dict] = [
    {
        "name": "edge_status",
        "description": "Book height, sealed periods, and this member's id. The cheapest "
                       "question: is this edge alive and how much has it recorded.",
        "inputSchema": {"type": "object", "properties": {}},
    },
    {
        "name": "edge_relations",
        "description": "Open relations: counterparty, role, contract reference. Structure "
                       "only — no amounts, no documents.",
        "inputSchema": {"type": "object", "properties": {}},
    },
    {
        "name": "edge_records",
        "description": "This member's own records, optionally for one relation. The exact "
                       "figure, currency, line SKUs and quantities, and the internal ERP "
                       "document reference are WITHHELD and named in a `withheld` field so "
                       "nothing is hidden about what is hidden. For content plus proof, "
                       "that is a disclosure decision — see edge_disclose.",
        "inputSchema": {
            "type": "object",
            "properties": {"relation_ref": {"type": "string",
                                            "description": "optional; omit for all"}},
        },
    },
    {
        "name": "edge_review",
        "description": "THE L0 TOOL. Compare this member's records against the "
                       "counterparty's for one relation and return the report: AGREED, "
                       "DIFFERS (with the differing fields named), SHORTFALL, ABSENT, "
                       "UNILATERAL (not a fault). Computed offline — no clock, no "
                       "session. This is what lets two parties settle it themselves.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "relation_ref": {"type": "string"},
                "counterparty_records": {"type": "array", "items": {"type": "object"}},
            },
            "required": ["relation_ref", "counterparty_records"],
        },
    },
    {
        "name": "edge_disclose",
        "description": "DISCLOSURE, not a read: hands over ONE record with the proof it was "
                       "in a sealed snapshot (record, record_hash, snapshot, path), so the "
                       "recipient re-derives the root themselves. This one DOES carry "
                       "content — that is what selective disclosure is for.",
        "inputSchema": {
            "type": "object",
            "properties": {"event_key": {"type": "string"}},
            "required": ["event_key"],
        },
    },
]


def tool_manifest() -> list[dict]:
    """The MCP `tools/list` payload. Kept as data so a host can render it without importing us."""
    return [dict(tool) for tool in TOOLS]


class EdgeMCPServer:
    """Dispatch MCP tool calls against ONE `NEOedge`.

    Construction takes the edge itself, so there is no configuration to get wrong and no way
    to point this at someone else's book: the server can only ever answer for the edge it
    was handed.
    """

    def __init__(self, edge: Any) -> None:
        self._edge = edge
        self._handlers: dict[str, Callable[..., Any]] = {
            "edge_status": self._status,
            "edge_relations": self._relations,
            "edge_records": self._records,
            "edge_review": self._review,
            "edge_disclose": self._disclose,
        }

    # ── dispatch ─────────────────────────────────────────────────────────────────────────

    def list_tools(self) -> list[dict]:
        return tool_manifest()

    def call(self, name: str, arguments: dict | None = None) -> dict:
        handler = self._handlers.get(name)
        if handler is None:
            return {"isError": True, "content": f"unknown tool: {name}"}
        try:
            return {"isError": False, "content": handler(**(arguments or {}))}
        except Exception as error:  # noqa: BLE001 — a reader must never take the edge down
            return {"isError": True, "content": f"{type(error).__name__}: {error}"}

    # ── tools ────────────────────────────────────────────────────────────────────────────

    def _status(self) -> dict:
        # book.verify() returns None and RAISES on a broken chain — so the honest boolean is
        # "did it refuse to raise", not its return value.
        try:
            self._edge.book.verify()
            verifies = True
        except Exception:  # noqa: BLE001
            verifies = False
        return {
            "member_id": self._edge.member_id,
            "book_height": self._edge.book.height,
            "sealed_periods": [s.period for s in self._edge.sealed],
            "verifies": verifies,
        }

    def _relations(self) -> list[dict]:
        seen: dict[str, dict] = {}
        contracts = {r.get("relation_ref"): r.get("originator_doc_no")
                     for r in self._edge.records() if r.get("doc_kind") == "CONTRACT"}
        for record in self._edge.records():
            ref = record.get("relation_ref")
            if ref and ref not in seen:
                seen[ref] = {"relation_ref": ref,
                             "counterparty": record.get("counterparty_id"),
                             "role": record.get("role"),
                             "contract_no": contracts.get(ref)}
        return list(seen.values())

    # Fields a record may carry that this tool must NEVER hand to a host.
    # Found by review 2026-08-01 (in the founding repo): the tool's description promised
    # structure-only while the code returned `amount`, `currency`, line SKUs and
    # quantities, and `own_doc_ref` — the company's internal ERP document number, company
    # code and fiscal year. A false promise in a tool DESCRIPTION is worse than a bug,
    # because the host reads it and trusts it. The description is true because the code
    # makes it true.
    _WITHHELD = ("amount", "currency", "own_doc_ref", "lines")

    def _records(self, relation_ref: str | None = None) -> list[dict]:
        records = self._edge.records()
        if relation_ref:
            records = [r for r in records if r.get("relation_ref") == relation_ref]
        out = []
        for record in records:
            safe = {k: v for k, v in record.items() if k not in self._WITHHELD}
            if "lines" in record:
                # shape without content: how many lines, never which SKU or how many units
                safe["line_count"] = len(record["lines"])
            if any(k in record for k in self._WITHHELD):
                safe["withheld"] = [k for k in self._WITHHELD if k in record]
            out.append(safe)
        return out

    def _review(self, relation_ref: str, counterparty_records: list[dict]) -> dict:
        relation = self._relation_by_ref(relation_ref)
        report = self._edge.review_against(counterparty_records, relation)
        return {
            "relation_ref": relation_ref,
            "agreed": report.agreed,
            "text": str(report),
            "disagreements": [
                {"status": getattr(d, "status", None), "event_key": getattr(d, "event_key", None),
                 "detail": str(d)}
                for d in getattr(report, "disagreements", [])
            ],
            # Reported APART from disagreements, never folded into them or into `agreed`.
            # This grammar has no NOT_COMPARABLE yet; the key stays so a host reading both
            # edge generations sees one shape, and so its absence is visible, not silent.
            "not_comparable": [
                {"status": getattr(n, "status", None), "event_key": getattr(n, "event_key", None),
                 "uncompared": list(getattr(n, "uncompared", [])), "detail": str(n)}
                for n in getattr(report, "not_comparable", [])
            ],
        }

    def _disclose(self, event_key: str) -> dict:
        """Return the PROOF, not a verdict about it.

        Found by review 2026-08-01 (founding repo): this once promised an offline-verifiable
        leaf-plus-path packet and returned only `verifies`, a path length and a byte count —
        our own assertion that it checks out, i.e. exactly the thing selective disclosure
        exists to make unnecessary. A recipient who cannot re-derive the root has been told,
        not shown.
        """
        d = self._edge.disclose(event_key)
        return {"event_key": event_key,
                "record": d.record, "record_hash": d.record_hash,
                "snapshot": d.snapshot, "path": d.path,
                "verifies_here": d.verify(),   # our own check — the recipient re-runs theirs
                "bytes": 32 + 32 * len(d.path),
                "note": "A disclosure is a DELIBERATE hand-over of one document with its "
                        "proof. Unlike edge_records this does carry content — that is what "
                        "it is for — so calling it is a disclosure decision, not a read."}

    # ── helpers ──────────────────────────────────────────────────────────────────────────

    def _relation_by_ref(self, relation_ref: str):
        """Rebuild the Relation from this edge's OWN records.

        The edge keeps no relation registry — `ref` is derived on both sides from the
        contract number and the two member ids, so it never needed one. The records
        therefore carry everything a `Relation` is, and reconstructing it here means the MCP
        surface can speak in refs (which a host can hold) instead of demanding an object
        (which it cannot serialise)."""
        from ..edge import Relation

        for record in self._edge.records():
            if record.get("relation_ref") != relation_ref:
                continue
            contract = next(
                (r["originator_doc_no"] for r in self._edge.records()
                 if r.get("relation_ref") == relation_ref
                 and r.get("doc_kind") == "CONTRACT"), "")
            return Relation(ref=relation_ref, contract_no=contract,
                            counterparty_id=record.get("counterparty_id", ""),
                            role=record.get("role", ""))
        raise KeyError(f"no records under relation {relation_ref!r} on this edge")
