"""``/api/clearhouse*`` — singleton clearhouse config + SFTP submission.

Three endpoints, all gated by ``matrix_gate``:

- ``GET  /api/clearhouse``         — read the singleton clearhouse row
  (dzinesco's identity, SFTP block, filename block). 404 when
  unseeded.
- ``PATCH /api/clearhouse``        — full-row replacement of the
  singleton (SP25). Strict-validates ``sftp_block`` first (Pydantic
  v2 default mode coerces strings-to-bools and would hide a real
  operator mistake), then validates the whole body in loose mode.
  Hot-reloads the running scheduler via
  ``scheduler.reconfigure_scheduler`` so the next tick picks up the
  new ``SftpBlock`` without a process restart.
- ``POST /api/clearhouse/submit``  — submit a batch of claims to
  the clearhouse. Stub: serializes via the SP7 serializer, builds
  an HCPF-compliant outbound filename, copies the result to the
  staging path. Per-claim audit events stamped with
  ``actor="clearhouse-submit"``.

Three single-router helpers stay in this file (per spec D4):

- :func:`_load_claim_row`            — load a ``Claim`` row by id.
- :func:`_serialize_claim_for_submit` — re-serialize a claim to X12
  with optional per-call kwargs (submitter, receiver, SBR09, etc).
- :func:`_serialize_claim_from_raw`  — best-effort serializer that
  re-parses stored ``x12_text`` and re-emits.

SP36 Task 11: this block moved here from ``api.py:2484`` (the 3
``/api/clearhouse*`` routes + 3 single-router helpers).
"""
from __future__ import annotations

import json
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Request

from cyclone import db
from cyclone.api_routers._shared import _actor_user_id
from cyclone.audit_log import AuditEvent, append_event
from cyclone.auth.deps import matrix_gate
from cyclone.db import Claim
from cyclone.store import store

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


@router.get("/api/clearhouse")
def get_clearhouse():
    """Return the singleton clearhouse config (dzinesco's identity, SFTP block, filename block)."""
    ch = store.get_clearhouse()
    if ch is None:
        raise HTTPException(status_code=404, detail="clearhouse not seeded")
    return json.loads(ch.model_dump_json())


@router.patch("/api/clearhouse")
async def patch_clearhouse(body: dict) -> Any:
    """Replace the singleton clearhouse row (SP25).

    The full ``Clearhouse`` model is required — we don't accept partial
    updates because the operator-facing use case is "I'm switching the
    loop to real MFT" or "I'm pointing at a different MFT server",
    not "I'm tweaking one field at a time." Validation errors are
    returned as 422 (Pydantic default).

    After a successful write, the running scheduler is hot-reloaded
    via ``scheduler.reconfigure_scheduler()`` so the next tick uses
    the new SftpBlock without a process restart.
    """
    from cyclone import scheduler as _scheduler_mod
    from cyclone.providers import Clearhouse as _Clearhouse, SftpBlock as _SftpBlock

    # Strict-validate the sftp_block sub-dict FIRST. Pydantic v2's
    # default mode coerces strings to bools (e.g. ``"stub": "yes"``
    # silently becomes True), which would hide a real operator
    # mistake. The Clearhouse model itself stays in loose mode so
    # ISO-string ``updated_at`` (the JSON round-trip shape) keeps
    # parsing.
    raw_sb = body.get("sftp_block", {})
    try:
        _SftpBlock.model_validate(raw_sb, strict=True)
    except Exception as exc:
        raise HTTPException(
            status_code=422, detail=f"invalid sftp_block: {exc}",
        ) from exc

    # Now validate the full body in loose mode.
    try:
        parsed = _Clearhouse.model_validate(body)
    except Exception as exc:
        raise HTTPException(
            status_code=422, detail=str(exc),
        ) from exc

    # SP25: when sftp_block.stub=false, the block must carry an auth
    # account name and a non-empty host. The Pydantic model catches
    # some of these; this catches the "empty password_keychain_account"
    # case (which Pydantic allows because it's a free-form dict).
    sb = parsed.sftp_block
    if not sb.stub:
        if not sb.host:
            raise HTTPException(
                status_code=422,
                detail="sftp_block.host is required when stub=false",
            )
        auth = sb.auth or {}
        if not auth.get("password_keychain_account") and not auth.get("key_file"):
            raise HTTPException(
                status_code=422,
                detail=(
                    "sftp_block.auth must contain either "
                    "'password_keychain_account' or 'key_file' when stub=false"
                ),
            )

    updated = store.update_clearhouse(parsed)
    await _scheduler_mod.reconfigure_scheduler(
        updated.sftp_block,
        sftp_block_name=updated.name or "default",
    )
    return json.loads(updated.model_dump_json())


@router.post("/api/clearhouse/submit")
def submit_to_clearhouse(request: Request, body: dict):
    """Submit a batch of claims to the clearhouse (SFTP). SP9: stub.

    Body: ``{"claim_ids": [...], "payer_id": "CO_TXIX"}``

    Stub behavior: serializes each claim via the SP7 serializer, builds
    an HCPF-compliant outbound filename, and copies the result to
    ``{staging_dir}/{outbound_path}/{filename}`` instead of opening a
    real SFTP connection. Returns a receipt per claim.
    """
    from cyclone.clearhouse import make_client
    from cyclone.edi.filenames import build_outbound_filename

    claim_ids = body.get("claim_ids", [])
    payer_id = body.get("payer_id")
    if not claim_ids:
        raise HTTPException(status_code=400, detail="claim_ids required")
    if not payer_id:
        raise HTTPException(status_code=400, detail="payer_id required")

    ch = store.get_clearhouse()
    if ch is None:
        raise HTTPException(status_code=500, detail="clearhouse not seeded")

    # Submitter (Loop 1000A) comes from the clearhouse config. The
    # receiver (NM1*40) and SBR09 come from the per-payer config and
    # are resolved per-claim below. Without this wiring, the
    # serializer would emit "CYCLONE" / "RECEIVER" placeholders and
    # the file would be rejected by HCPF.
    submitter_kwargs = {
        "sender_id": ch.tpid,
        "submitter_name": ch.submitter_name,
        "submitter_contact_name": ch.submitter_contact_name,
        "submitter_contact_email": ch.submitter_contact_email,
    }
    if getattr(ch, "submitter_contact_phone", None):
        submitter_kwargs["submitter_contact_phone"] = ch.submitter_contact_phone

    # Build a payer_id → PayerConfig837 map once so we can look up the
    # receiver + SBR09 default for each claim.
    from cyclone.db import PayerConfigORM as _PayerConfigORM

    def _resolve_payer_cfg(claim_obj) -> dict | None:
        pid = (claim_obj.payer.id or "").strip() if claim_obj.payer else ""
        pname = (claim_obj.payer.name or "").strip() if claim_obj.payer else ""
        with db.SessionLocal()() as ss:
            if pid:
                row = ss.get(_PayerConfigORM, (pid, "837P"))
                if row is not None:
                    return dict(row.config_json)
            if pname:
                rows = (
                    ss.query(_PayerConfigORM)
                    .filter(_PayerConfigORM.transaction_type == "837P")
                    .all()
                )
                for r in rows:
                    cj = dict(r.config_json)
                    if (r.payer_id or "").upper() == pname.upper():
                        return cj
                    if pname.lower() in str(cj).lower():
                        return cj
            row = (
                ss.query(_PayerConfigORM)
                .filter(_PayerConfigORM.transaction_type == "837P")
                .first()
            )
            return dict(row.config_json) if row else None

    client = make_client(ch.sftp_block)
    results = []
    for cid in claim_ids:
        try:
            x12_text = _serialize_claim_for_submit(cid)
        except Exception as exc:  # noqa: BLE001
            results.append({"claim_id": cid, "ok": False, "error": str(exc)})
            continue
        # Re-resolve the claim so we can look up its payer config. We
        # re-parse the stored x12_text to get a ClaimOutput (same path
        # the serializer uses).
        try:
            from cyclone.parsers.parse_837 import parse as _parse837
            claim_row_obj = _load_claim_row(cid)
            if claim_row_obj is None or not (claim_row_obj.raw_json or {}).get("x12_text"):
                raise RuntimeError("no stored x12_text for claim")
            parsed = _parse837(claim_row_obj.raw_json["x12_text"])
            claim_obj = parsed.claims[0] if parsed.claims else None
        except Exception:
            claim_obj = None
        if claim_obj is not None:
            payer_cfg = _resolve_payer_cfg(claim_obj) or {}
            receiver_id = payer_cfg.get("receiver_id") or (claim_obj.payer.id if claim_obj.payer else None) or "RECEIVER"
            receiver_name = payer_cfg.get("receiver_name") or (claim_obj.payer.name if claim_obj.payer else None) or receiver_id
            sbr09 = payer_cfg.get("sbr09_default") or "MC"
            # Re-serialize with the proper envelope values.
            try:
                x12_text = _serialize_claim_for_submit(
                    cid,
                    **{**submitter_kwargs, "receiver_id": receiver_id,
                       "receiver_name": receiver_name,
                       "claim_filing_indicator_code": sbr09},
                )
            except Exception as exc:  # noqa: BLE001
                results.append({"claim_id": cid, "ok": False, "error": str(exc)})
                continue
        filename = build_outbound_filename(ch.tpid, "837P")
        remote = f"{ch.sftp_block.paths['outbound']}/{filename}"
        staging_path = client.write_file(remote, x12_text.encode("utf-8"))
        results.append({
            "claim_id": cid,
            "ok": True,
            "filename": filename,
            "staging_path": str(staging_path),
            "remote_path": remote,
        })
        # SP11: audit trail for each successful clearhouse submission.
        with db.SessionLocal()() as audit_s:
            append_event(audit_s, AuditEvent(
                event_type="clearhouse.submitted",
                entity_type="claim",
                entity_id=cid,
                payload={
                    "filename": filename,
                    "remote_path": remote,
                    "tpid": ch.tpid,
                    "stub": ch.sftp_block.stub,
                },
                actor="clearhouse-submit",
                user_id=_actor_user_id(request),
            ))
            audit_s.commit()
    return {"ok": True, "submitted": results, "stub": ch.sftp_block.stub}


def _load_claim_row(claim_id: str):
    """Helper: load a Claim row by id (or return None)."""
    with db.SessionLocal()() as s:
        return s.get(Claim, claim_id)


def _serialize_claim_for_submit(claim_id: str, **kwargs) -> str:
    """Serialize a claim to X12 for SFTP submission. Lazy import of the
    serializer to avoid pulling FastAPI machinery at module import time.

    Optional ``**kwargs`` are forwarded to the serializer — used to
    pass through clearhouse submitter info and per-payer receiver info
    so the regenerated file matches what the HCPF MFT expects.
    """
    from cyclone.parsers.serialize_837 import serialize_837
    from cyclone import db
    with db.SessionLocal()() as s:
        row = s.get(db.Claim, claim_id)
        if row is None:
            raise ValueError(f"claim {claim_id!r} not found")
        # Re-parse the stored raw_json to get a ClaimOutput
        from cyclone.parsers.models import ClaimOutput, Envelope, Subscriber, Payer, BillingProvider
        from cyclone.parsers.parse_837 import parse
        raw = row.raw_json or {}
        # Reconstruct minimal ClaimOutput from raw_json; this is best-effort.
        return _serialize_claim_from_raw(row, raw, **kwargs)


def _serialize_claim_from_raw(claim_row, raw: dict, **kwargs) -> str:
    """Best-effort serializer that uses the stored raw_json to emit a fresh 837.

    For SP9 this delegates to the existing serialize_837 helper if the
    claim has a complete raw_segments array. Otherwise it returns a
    minimal placeholder. ``**kwargs`` are forwarded to the serializer
    so callers can pass through submitter / receiver / SBR09 values
    from the clearhouse and per-payer configs.
    """
    from cyclone.parsers.serialize_837 import serialize_837
    from cyclone.parsers.parse_837 import parse

    # Re-parse the original batch text (need to re-derive from store).
    # SP9 stub: if the claim has a `raw_json` with `x12_text`, use that.
    if isinstance(raw, dict) and raw.get("x12_text"):
        result = parse(raw["x12_text"])
        if result.claims:
            return serialize_837(result.claims[0], **kwargs)
    # Fallback: raise so the caller sees an error.
    raise RuntimeError(
        f"claim {claim_row.id!r} cannot be re-serialized: no stored x12_text"
    )
