"""SP41 — rebill admin endpoints.

POST /api/admin/rebill-from-835
  body: {"window": "YYYY-MM-DD..YYYY-MM-DD",
         "override_filing": bool,
         "visits_csv_path": str (optional),
         "ingest_dir": str (optional),
         "out_dir": str (optional)}
  Returns: {"summary_path": str, "counts": {...},
            "pipeline_a_files": [...], "pipeline_b_files": [...]}

GET /api/admin/rebill-from-835/status
  Returns: {"recent_runs": [{"as_of": ..., "summary_path": ...,
                              "counts": {...}}, ...]}

Status-code contract (per the cyclone-api-router / cyclone-cli skills):
  - 200: completed run (POST) or tally returned (GET).
  - 401: not authenticated (matrix_gate).
  - 403: authenticated but not authorized for /api/admin/*.
  - 422: window is malformed or Pydantic body validation failed.

The POST handler delegates to ``cyclone.rebill.run.run_rebill`` (the same
orchestrator the ``cyclone rebill-from-835`` CLI uses). The GET handler
is a filesystem scan under ``dev/rebills/*/summary.csv`` — no DB table
for "recent runs" exists today, and Task 12 deliberately doesn't add
one (per its design notes). Sorted by directory mtime descending;
truncated to the most recent 5.
"""
from __future__ import annotations

import csv
import logging
from datetime import date
from pathlib import Path
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, ConfigDict, Field, field_validator

from cyclone.auth.deps import matrix_gate
from cyclone.rebill.run import run_rebill

log = logging.getLogger(__name__)

router = APIRouter(
    prefix="/api/admin/rebill-from-835",
    tags=["rebill"],
    dependencies=[Depends(matrix_gate)],
)


# Filesystem base for /status scans. Module-level so tests can monkeypatch
# it to a tmp_path-relative dir without chdir-ing the whole test process.
REBILLS_DIR: Path = Path("dev/rebills")

# How many recent runs /status surfaces. 5 matches the CLI's default; the
# body schema below mirrors it as a query parameter so callers can ask for
# more (or fewer) when they want.
DEFAULT_RECENT_LIMIT = 5


class RebillRequest(BaseModel):
    """Body schema for ``POST /api/admin/rebill-from-835``.

    ``window`` is parsed as ``YYYY-MM-DD..YYYY-MM-DD`` (inclusive both
    ends); the validator rejects anything else with a 422. The other
    path fields are optional — ``run_rebill`` falls back to its own
    defaults (``data/source/apr-jun27.csv`` for visits, ``ingest/`` for
    835s, ``dev/rebills/<today>/`` for output) when unset.
    """

    model_config = ConfigDict(populate_by_name=True)

    window: str = Field(
        default="2026-01-01..2026-06-27",
        description="DOS window as YYYY-MM-DD..YYYY-MM-DD (inclusive both ends).",
    )
    override_filing: bool = Field(
        default=False,
        description="Relax the 120-day timely-filing gate for past-window visits.",
    )
    visits_csv_path: str | None = Field(
        default=None,
        description="Path to the AxisCare visits CSV; defaults to "
                    "data/source/apr-jun27.csv when unset.",
    )
    ingest_dir: str | None = Field(
        default=None,
        description="Directory containing *.835 / *.x12 835 files; defaults to ./ingest.",
    )
    out_dir: str | None = Field(
        default=None,
        description="Output directory for summary.csv + pipeline-a/b files; "
                    "defaults to dev/rebills/<today>/.",
    )

    @field_validator("window")
    @classmethod
    def _validate_window(cls, v: str) -> str:
        # Manual split (Click can't help here). Two-date range is the
        # only accepted shape; ``..`` is the giveaway separator so the
        # input is unambiguous.
        try:
            start_str, end_str = v.split("..", 1)
        except ValueError as exc:
            raise ValueError(
                f"window must be 'YYYY-MM-DD..YYYY-MM-DD', got {v!r}"
            ) from exc
        try:
            start = date.fromisoformat(start_str)
            end = date.fromisoformat(end_str)
        except ValueError as exc:
            raise ValueError(
                f"window must be 'YYYY-MM-DD..YYYY-MM-DD', got {v!r}: {exc}"
            ) from exc
        if start > end:
            raise ValueError(
                f"window start {start.isoformat()} is after end {end.isoformat()}"
            )
        return v


@router.post("")
def post_rebill(req: RebillRequest) -> dict[str, Any]:
    """Run the rebill pipeline for the given DOS window.

    Delegates to :func:`cyclone.rebill.run.run_rebill` and returns the
    resulting ``RunResult`` as a plain JSON dict (the underlying dataclass
    has no ``to_dict`` method — fields are mapped here so callers don't
    have to import the dataclass shape).

    The handler does NOT swallow exceptions; the app-level
    :func:`_unhandled_exception_handler` renders them as a 500 JSON
    envelope with CORS headers. Per-file failures land in the summary
    CSV (and the ``counts`` dict) rather than raising here.
    """
    start_str, end_str = req.window.split("..", 1)
    # Validator already proved these parse cleanly.
    window_start = date.fromisoformat(start_str)
    window_end = date.fromisoformat(end_str)

    log.info(
        "rebill-from-835 starting: window=%s override_filing=%s",
        req.window, req.override_filing,
    )
    result = run_rebill(
        window_start=window_start,
        window_end=window_end,
        override_filing=req.override_filing,
        visits_csv_path=req.visits_csv_path,
        ingest_dir=req.ingest_dir,
        out_dir=req.out_dir,
    )
    log.info(
        "rebill-from-835 done: summary=%s counts=%s",
        result.summary_path, result.counts,
    )

    return {
        "summary_path": str(result.summary_path),
        "counts": result.counts,
        "pipeline_a_files": [str(p) for p in result.pipeline_a_files],
        "pipeline_b_files": [str(p) for p in result.pipeline_b_files],
    }


@router.get("/status")
def get_rebill_status(
    limit: int = DEFAULT_RECENT_LIMIT,
) -> dict[str, Any]:
    """Return the most recent rebill runs, newest first.

    Scans :data:`REBILLS_DIR` (``dev/rebills/`` by default) for any
    subdirectory whose name is a date in YYYY-MM-DD format and that
    contains a ``summary.csv``. Sorted by directory mtime descending so
    the most recently-run batch wins ties on equal-named dirs (rare in
    practice — operators tend to pick a fresh date per run).

    Each entry's ``counts`` dict is a tally of the summary.csv's
    ``disposition`` column (the same per-category counters the operator
    sees on the CLI ``--status`` view). Missing disposition values
    surface as ``UNKNOWN`` so a hand-edited CSV can't silently drop a
    bucket.

    ``limit`` defaults to :data:`DEFAULT_RECENT_LIMIT` (5) and is
    clamped to ``[1, 50]`` to keep the response bounded.
    """
    if limit < 1 or limit > 50:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="limit must be between 1 and 50",
        )

    recent: list[dict[str, Any]] = []
    rebills_dir = REBILLS_DIR
    if rebills_dir.exists():
        candidates = sorted(
            (
                d for d in rebills_dir.iterdir()
                if d.is_dir()
                and (d / "summary.csv").exists()
                # Filter to date-named dirs (YYYY-MM-DD) so a stray
                # ``lost+found`` or tempdir doesn't sneak in.
                and len(d.name) == 10 and d.name[4] == "-" and d.name[7] == "-"
            ),
            key=lambda d: d.stat().st_mtime,
            reverse=True,
        )[:limit]
        for d in candidates:
            summary_path = d / "summary.csv"
            counts: dict[str, int] = {}
            with summary_path.open(newline="") as f:
                for row in csv.DictReader(f):
                    disp = (row.get("disposition", "UNKNOWN") or "").strip() or "UNKNOWN"
                    counts[disp] = counts.get(disp, 0) + 1
            recent.append({
                "as_of": d.name,
                "summary_path": str(summary_path),
                "counts": counts,
            })
    return {"recent_runs": recent}
