"""SP37 Task 6: HTTP endpoint for the canonical submit-batch flow.

Thin wrapper around ``cyclone.submission.submit_file`` — same logic as
the ``cyclone submit-batch`` CLI (SP37 Task 5), just framed as JSON in
/ JSON out and gated by ``matrix_gate``. The walker pattern, ``._*``
AppleDouble skip, ``limit`` semantics, and per-file outcomes all match
the CLI byte-for-byte so a batch run via the CLI and the same batch
run via this endpoint produce identical DB + SFTP state.

The endpoint deliberately does NOT inject an ``sftp_client_factory``:
``submit_file`` defaults to its paramiko-based factory so SKIPPED is
reachable in production (the ``SftpClient`` wrapper has no ``stat()``).
Tests monkey-patch ``cyclone.api_routers.submission.submit_file``
itself; that avoids the paramiko factory entirely without touching
the helper's contract.

Status code contract (per Task 6 spec §4):
  - 200: completed run. Per-file failures live in the JSON body.
  - 401: not authenticated (matrix_gate).
  - 404: no clearhouse seeded (config-level "missing" → 4xx, not 5xx).
  - 409: clearhouse SFTP block is in stub mode (refuses to upload).
  - 422: ``ingest_dir`` missing on disk OR Pydantic body validation
    failed (missing fields, wrong types).
  - 5xx: truly unexpected exceptions propagate (do not swallow).
"""
from __future__ import annotations

import logging
from pathlib import Path

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

from cyclone.auth.deps import matrix_gate
from cyclone.store import store as cycl_store
from cyclone.store.exceptions import DuplicateClaimError
from cyclone.submission.core import submit_file
from cyclone.submission.result import SubmitOutcome, SubmitResult

log = logging.getLogger(__name__)

# No `prefix=` here — every other gated router in this package declares
# the full path in the decorator (clearhouse.py uses "/api/clearhouse",
# parse.py uses "/api/parse-837", etc.). The decorator sets the full URL.
router = APIRouter(
    tags=["submission"],
    dependencies=[Depends(matrix_gate)],
)


class SubmitBatchRequest(BaseModel):
    """Body schema for ``POST /api/submit-batch``.

    ``ingest_dir`` has no default so Pydantic raises 422 when it's
    missing — better UX than letting the walker crash on a missing
    path. ``validate_files`` and ``actor`` default so a minimal client
    can skip them. ``limit`` truncates the file list after the walker
    collects it (mirrors the CLI's post-collection ``if i > limit:
    break`` semantics, but applied as a slice since the HTTP body
    model is type-checked up-front).

    The ``validate_files`` field is aliased to ``validate`` in the JSON
    body to mirror the CLI's ``--validate`` flag and avoid the
    hardcoded Pydantic warning about ``validate`` shadowing
    ``BaseModel.validate``. ``populate_by_name=True`` lets tests
    construct the model with either key.
    """
    model_config = ConfigDict(populate_by_name=True, protected_namespaces=())

    ingest_dir: str
    validate_files: bool = Field(default=True, alias="validate")
    actor: str = "api-submit-batch"
    limit: int | None = None


@router.post("/api/submit-batch")
def submit_batch(body: SubmitBatchRequest):
    """Submit every ``batch-*-claims/*.x12`` under ``ingest_dir``.

    Walks ``ingest_dir`` for any directory matching ``batch-*-claims``,
    collects each one's ``*.x12`` files (sorted, with ``._*``
    AppleDouble files skipped), truncates to ``limit`` if set, then
    calls :func:`cyclone.submission.submit_file` per file with the
    seeded clearhouse's ``sftp_block`` and ``actor`` from the body.

    Returns counts (``submitted`` / ``skipped`` / ``failed``) plus a
    per-file ``results`` array. Per-file failures NEVER change the
    HTTP status code — the response is 200 whenever the run itself
    completed.
    """

    # 1. Config-level guards. Order matters: a missing clearhouse is a
    #    404 (config-level "missing"), but if it IS present and in stub
    #    mode the operator's request is a 409 (configured-but-wrong).
    clearhouse = cycl_store.get_clearhouse()
    if clearhouse is None:
        raise HTTPException(
            status_code=404, detail="no clearhouse seeded",
        )
    sftp_block = clearhouse.sftp_block
    if sftp_block.stub:
        raise HTTPException(
            status_code=409, detail="clearhouse SFTP block is in stub mode",
        )

    # 2. Resolve + validate the ingest dir. Use ``resolve()`` so a
    #    symlink-relative path still produces a stable error message.
    root = Path(body.ingest_dir).resolve()
    if not root.exists():
        raise HTTPException(
            status_code=422,
            detail=f"ingest_dir does not exist: {root}",
        )

    # 3. Walker — must match the CLI EXACTLY. Same sort, same ``._*``
    #    AppleDouble skip. Any drift here is a quiet split between the
    #    two surfaces and silently produces different batch outcomes.
    files: list[Path] = []
    for batch_dir in sorted(root.glob("batch-*-claims")):
        files.extend(sorted(
            p for p in batch_dir.glob("*.x12")
            if not p.name.startswith("._")
        ))

    # 4. ``limit`` truncates after collection. The CLI uses an inline
    #    ``if i > limit: break``; we slice instead because the HTTP
    #    body model validates ``limit`` up-front (Pydantic-level int
    #    check) and slicing keeps the walker branchless.
    if body.limit is not None:
        files = files[: body.limit]

    if not files:
        raise HTTPException(
            status_code=422,
            detail=f"no batch-*-claims/*.x12 files found under {root}",
        )

    # 5. Per-file submit. Wrap the helper call in try/except so an
    #    unexpected exception in submit_file surfaces as a per-file
    #    failure (outcome="unexpected") instead of crashing the whole
    #    run. The helper's own SubmitOutcome enum covers every typed
    #    failure path; an uncaught exception here is a true
    #    surprise (bug or service outage mid-loop).
    results: list[dict] = []
    submitted = skipped = failed = 0
    for src in files:
        try:
            r = submit_file(
                src,
                sftp_block=sftp_block,
                actor=body.actor,
                validate=body.validate_files,
                # No ``sftp_client_factory`` — submit_file's default
                # paramiko factory opens the real MFT. Tests
                # monkey-patch submit_file itself instead of wiring a
                # factory here.
            )
        except DuplicateClaimError as exc:
            # SP41 Task 9: 409-class domain exception (see the exception
            # docstring on ``DuplicateClaimError``). The per-file loop
            # preserves the 200-with-results status-code contract
            # documented at the top of this module, so we surface the
            # duplicate as a per-file failure with UNEXPECTED_ERROR
            # outcome — the structured ``claim_id`` / ``original_submission_at``
            # attributes ride along in the error string so the operator
            # can see which CLM01 in the batch tripped the guard. Caught
            # BEFORE the generic ``Exception`` handler so a future
            # caller propagating the exception still gets 409-class
            # treatment at the FastAPI layer.
            log.warning(
                "submit-batch duplicate claim on %s: claim_id=%r original=%s",
                src.name, exc.claim_id, exc.original_submission_at,
            )
            r = SubmitResult(
                file=src.name,
                outcome=SubmitOutcome.UNEXPECTED_ERROR,
                error=(
                    f"DuplicateClaimError: claim_id {exc.claim_id!r} was "
                    f"already submitted at "
                    f"{exc.original_submission_at.isoformat()}; "
                    f"within 30-day window"
                ),
            )
        except Exception as exc:  # noqa: BLE001
            log.exception(
                "submit-batch unexpected error on %s", src.name,
            )
            r = SubmitResult(
                file=src.name,
                outcome=SubmitOutcome.UNEXPECTED_ERROR,
                error=f"{exc.__class__.__name__}: {exc}",
            )

        if r.outcome == SubmitOutcome.SUBMITTED:
            submitted += 1
        elif r.outcome == SubmitOutcome.SKIPPED:
            skipped += 1
        else:
            failed += 1

        results.append({
            "file": r.file,
            "outcome": r.outcome.value,
            "batch_id": r.batch_id,
            "error": r.error,
        })

    return {
        "submitted": submitted,
        "skipped": skipped,
        "failed": failed,
        "results": results,
    }