"""Reconciliation read views + manual match/unmatch write paths.

Four endpoints:

- ``GET  /api/reconciliation/unmatched`` — list of unmatched Claims
  and unmatched Remittances (``store.list_unmatched(kind="both")``).
- ``GET  /api/batch-diff``             — side-by-side diff of two
  batches (used by the Batch Diff page). Lazy-imports
  :func:`cyclone.batch_diff.diff_batches_to_wire` to keep the
  module's import surface small until the endpoint is actually
  hit.
- ``POST /api/reconciliation/match``   — manually pair a Claim with
  a Remittance (``store.manual_match``). Surfaces ``AlreadyMatchedError`` /
  ``InvalidStateError`` as 409, and ``LookupError`` from the store
  as 404 (``claim_or_remit_not_found``).
- ``POST /api/reconciliation/unmatch`` — unpair a Claim (``store.manual_unmatch``).
  Surfaces ``NotMatchedError`` as 409.

All four are read-or-manual-override surfaces used by the
Reconciliation page (the page that pairs Claims with Remittances
when neither side has an automatic match key).

SP36 Task 8: this block moved here from ``api.py:2450`` (the
4 routes interleaved with a side-by-side batch-diff divider).
"""
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, Query

from cyclone.auth.deps import matrix_gate
from cyclone.store import (
    AlreadyMatchedError,
    InvalidStateError,
    store,
)

router = APIRouter(dependencies=[Depends(matrix_gate)])


@router.get("/api/reconciliation/unmatched")
def get_reconciliation_unmatched() -> dict:
    """Return unmatched Claims (left) and unmatched Remittances (right).

    Powers the reconciliation review surface: every Claim with no
    paired Remittance appears on the left, every Remittance with no
    paired Claim appears on the right. The two lists are always present
    (empty list, never absent) so the UI can index unconditionally.
    """
    return store.list_unmatched(kind="both")


# --------------------------------------------------------------------------- #
# Side-by-side diff between two batches (SP3 P4 / T18)
# --------------------------------------------------------------------------- #


@router.get("/api/batch-diff")
def get_batch_diff(
    a: str | None = Query(None),
    b: str | None = Query(None),
) -> dict:
    """Return a side-by-side diff of two batches identified by id.

    Query params: ``a=<batch_id>``, ``b=<batch_id>`` (both required).

    Response body (snake_case keys, see :mod:`cyclone.batch_diff` for the
    projector shapes):
      - ``a`` / ``b`` — small metadata blocks (id, kind, parsedAt,
        inputFilename, claimCount)
      - ``added`` — claims present in B but not A
      - ``removed`` — claims present in A but not B
      - ``changed`` — claims present in both, with field deltas
      - ``summary`` — precomputed counts

    Errors:
      - 400 — missing ``a`` or ``b``
      - 404 — either batch id is unknown

    Pure read endpoint — never mutates the store. Both 837P and 835
    batches are accepted (mixed-kind diffs are valid: comparing the
    submitted claims against the matching remittances).
    """
    if not a or not b:
        raise HTTPException(
            status_code=400,
            detail={"error": "Missing param", "detail": "Both ?a=<batch_id> and ?b=<batch_id> are required."},
        )
    try:
        a_rec, b_rec = store.load_two_for_diff(a, b)
    except LookupError as exc:
        raise HTTPException(
            status_code=404,
            detail={"error": "Not found", "detail": str(exc)},
        )

    # Lazy import — keeps the module's import surface small until the
    # endpoint is actually hit. Mirrors the same pattern used by other
    # endpoint-local helpers (e.g. reconciler).
    from cyclone.batch_diff import diff_batches_to_wire

    return diff_batches_to_wire(a_rec, b_rec)


@router.post("/api/reconciliation/match")
def post_reconciliation_match(body: dict) -> dict:
    """Manually pair a Claim with a Remittance (operator override).

    Body: ``{"claim_id": ..., "remit_id": ...}``. Returns
    ``{"claim": <ui>, "match": <ui>}`` on success. Errors:
      - 400: missing ``claim_id`` or ``remit_id``
      - 404: claim or remittance not found
      - 409: claim already matched, or apply_* returned a noop
        (claim in terminal state) — detail echoes ``current_state``
        and ``activity_kind`` so the UI can render a precise message.
    """
    claim_id = body.get("claim_id")
    remit_id = body.get("remit_id")
    if not claim_id or not remit_id:
        raise HTTPException(
            status_code=400,
            detail="claim_id and remit_id required",
        )
    try:
        return store.manual_match(claim_id, remit_id)
    except AlreadyMatchedError as e:
        raise HTTPException(
            status_code=409,
            detail={"error": "already_matched", "message": str(e)},
        )
    except InvalidStateError as e:
        raise HTTPException(
            status_code=409,
            detail={
                "error": "invalid_state",
                "current_state": e.current_state,
                "activity_kind": e.activity_kind,
            },
        )
    except LookupError:
        # manual_match raises LookupError when the claim or remittance
        # row is missing (we catch the parent class so any future
        # KeyError subclasses in the store get the same treatment).
        raise HTTPException(
            status_code=404,
            detail="claim_or_remit_not_found",
        )


@router.post("/api/reconciliation/unmatch")
def post_reconciliation_unmatch(body: dict) -> dict:
    """Remove the current match for a Claim; reset Claim to submitted.

    Body: ``{"claim_id": ...}``. Returns
    ``{"claim": <ui>, "deletedMatches": <count>}``. Errors:
      - 400: missing ``claim_id``
      - 404: claim not found
      - 409: claim has no current match (NotMatchedError is mapped
        by the store; we surface 409 to match the manual_match contract)
    """
    from cyclone.store import NotMatchedError
    claim_id = body.get("claim_id")
    if not claim_id:
        raise HTTPException(
            status_code=400,
            detail="claim_id required",
        )
    try:
        return store.manual_unmatch(claim_id)
    except NotMatchedError as e:
        raise HTTPException(
            status_code=409,
            detail={"error": "not_matched", "message": str(e)},
        )
    except LookupError:
        raise HTTPException(
            status_code=404,
            detail="claim_not_found",
        )
