"""Batch read APIs and the legacy _BatchesShim.

The ``_BatchesShim`` class is retained for back-compat with the
``with store._lock: store._batches.clear()`` test-cleanup idiom used
in 7 test files. Its ``clear()`` method wipes all rows from the DB
tables so tests that depended on a fresh in-memory list per-test get
a fresh DB state per-test.
"""

from __future__ import annotations

from datetime import timezone

from cyclone import db
from cyclone.db import (
    ActivityEvent,
    Batch,
    CasAdjustment,
    Claim,
    Match,
    Remittance,
)
from cyclone.parsers.models import ParseResult
from cyclone.parsers.models_835 import ParseResult835

from .records import BatchRecord, BatchRecord837, BatchRecord835


class _BatchesShim:
    """Drop-in replacement for the old in-memory ``_batches`` list.

    ``clear()`` removes every row from the DB tables in FK-safe order.
    Other list operations are not implemented because the only call site
    is the ``clear()`` inside the test fixtures (``test_api_gets.py`` and
    ``test_api_parse_persists.py``).
    """

    def clear(self) -> None:  # type: ignore[no-untyped-def]
        with db.SessionLocal()() as s:
            s.query(ActivityEvent).delete()
            s.query(Match).delete()
            s.query(CasAdjustment).delete()
            s.query(Remittance).delete()
            s.query(Claim).delete()
            s.query(Batch).delete()
            s.commit()


def _row_to_record(row: Batch) -> BatchRecord:
    """Rehydrate a ``BatchRecord`` (837 or 835) from a Batch ORM row.

    The full ``ParseResult`` / ``ParseResult835`` lives in
    ``raw_result_json`` (stashed at insert time). Re-parsing JSON
    here means callers get the same typed Pydantic object the old
    in-memory store handed out, so api.py and tests that do
    ``rec.result.claims`` keep working unchanged.

    SQLite drops tz info on round-trip even though the column type
    is ``DateTime(timezone=True)``. We re-attach UTC so the
    ``BatchRecord`` validator (``parsed_at must be tz-aware``)
    passes.
    """
    if row.kind == "835":
        result_cls = ParseResult835
    else:
        result_cls = ParseResult
    payload = row.raw_result_json or {}
    result = result_cls.model_validate(payload)
    parsed_at = row.parsed_at
    if parsed_at is not None and parsed_at.tzinfo is None:
        parsed_at = parsed_at.replace(tzinfo=timezone.utc)
    record_cls = BatchRecord835 if row.kind == "835" else BatchRecord837
    return record_cls(
        id=row.id,
        kind=row.kind,
        input_filename=row.input_filename,
        parsed_at=parsed_at,
        result=result,
    )


def get_batch(batch_id: str) -> dict | None:
    """Return a summary dict for ``batch_id`` or ``None`` if missing.

    The dict shape matches what ``/api/batches/{id}`` callers need:
    ``id``, ``kind``, ``input_filename``, ``parsed_at``, and the
    full ``result`` (raw_result_json) as a dict.
    """
    with db.SessionLocal()() as s:
        row = s.get(Batch, batch_id)
        if row is None:
            return None
        return {
            "id": row.id,
            "kind": row.kind,
            "input_filename": row.input_filename,
            "parsed_at": row.parsed_at,
            "result": row.raw_result_json,
        }


def get_record(batch_id: str) -> BatchRecord | None:
    """Return the ``BatchRecord`` for ``batch_id`` or ``None``.

    Preserves the in-memory store contract: callers get a Pydantic
    ``BatchRecord`` (subclass ``BatchRecord837`` / ``BatchRecord835``)
    with ``.id``, ``.kind``, ``.input_filename``, ``.parsed_at``,
    and ``.result`` (typed ``ParseResult`` / ``ParseResult835``).
    """
    with db.SessionLocal()() as s:
        row = s.get(Batch, batch_id)
        if row is None:
            return None
        return _row_to_record(row)


def list_batches(*, limit: int = 100) -> list[BatchRecord]:
    """Return up to ``limit`` ``BatchRecord``s, newest first."""
    with db.SessionLocal()() as s:
        rows = (
            s.query(Batch)
            .order_by(Batch.parsed_at.desc())
            .limit(limit)
            .all()
        )
        return [_row_to_record(r) for r in rows]


def all_batches() -> list[BatchRecord]:
    """Return every ``BatchRecord``, oldest first (no pagination)."""
    with db.SessionLocal()() as s:
        rows = s.query(Batch).order_by(Batch.parsed_at.asc()).all()
        return [_row_to_record(r) for r in rows]


def load_two_for_diff(a_id: str, b_id: str) -> tuple[BatchRecord, BatchRecord]:
    """Load two batches by id for the side-by-side diff view.

    Returns ``(a, b)`` as ``BatchRecord`` objects. Raises
    :class:`LookupError` when either id is missing — the API layer
    catches it and maps it to ``404 Not Found`` (matching the
    ``GET /api/batches/{id}`` contract). The two loads happen in
    independent sessions so a transient failure on one side can't
    poison the other.

    Used exclusively by :mod:`cyclone.batch_diff` via the
    ``/api/batch-diff`` endpoint.
    """
    a = get_record(a_id)
    if a is None:
        raise LookupError(f"batch {a_id} not found")
    b = get_record(b_id)
    if b is None:
        raise LookupError(f"batch {b_id} not found")
    return a, b