#!/usr/bin/env python3
"""One-shot LOCAL pipeline for the 'run-halacha' button (halacha validation).

The /api/cases/{case}/final/run-halacha endpoint wakes the Hermes curator, which
runs THIS single deterministic command (the 3-judge panel uses local DeepSeek+Gemini
keys + the local claude CLI, so it can't run inside the container).

Steps:
  [0] precedent_extract_halachot → extract the halachot the DECISION ITSELF states
      (its own case_law row), so they aren't left pending. Idempotent.
  [1] extract_internal_citations(chair) → links the citation graph for the chair's
      decisions (idempotent; ON CONFLICT DO NOTHING).
  [2] corroboration_rebuild → builds the citation-treatment signal and applies the
      corroborated→approved / overruled→pending policy (X11 Phase 2).
  [3] halacha_panel_approve --apply → 3 judges (Opus+DeepSeek+Gemini); agreement
      auto-approves/rejects (reversible, CSV-backed); splits/defects → chair (INV-G10).

NB: per-precedent halacha extraction for newly-cited precedents is NOT automated here
(it needs each cited precedent to be in the library with a known case_law_id) — the
chair drives that from /precedents when a missing precedent is added.

Local-only. Idempotent. The panel pass over the full pending queue can take minutes.

Durable (X16 / INV-DUR1): the 4 steps run through scripts/_pipeline_runtime.py
with a SQLite checkpoint per case (data/checkpoints/halacha.sqlite). A crash/OOM
in the long panel [3] RESUMES from [3] on the next run instead of re-paying
[0]–[2]. Default = auto-resume an interrupted run; ``--fresh`` forces a clean run
from [0]. Requires the host extra ``pip install -e ".[durable]"`` (mcp-server);
without it the steps run linearly (same as before) — the button never breaks.

    cd ~/legal-ai/mcp-server
    .venv/bin/python ../scripts/final_halacha_pipeline.py --case 8126-03-25
    .venv/bin/python ../scripts/final_halacha_pipeline.py --case 8126-03-25 --fresh
"""
from __future__ import annotations

import argparse
import asyncio
import json
import sys
from argparse import Namespace
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

import _pipeline_runtime  # noqa: E402 — durable runtime (X16); scripts/ on sys.path
from legal_mcp import config  # noqa: E402
from legal_mcp.services import corroboration, db  # noqa: E402
from legal_mcp.tools.citations import extract_internal_citations  # noqa: E402
from legal_mcp.tools.precedent_library import precedent_extract_halachot  # noqa: E402


async def _decision_law_row(case_number: str) -> dict | None:
    """The case's own decision row in case_law (internal_committee), if enrolled."""
    pool = await db.get_pool()
    async with pool.acquire() as conn:
        r = await conn.fetchrow(
            "SELECT id, halacha_extraction_status FROM case_law WHERE case_number = $1 "
            "AND source_kind = 'internal_committee' ORDER BY created_at DESC LIMIT 1",
            case_number,
        )
    return dict(r) if r else None


async def main(args: argparse.Namespace) -> int:
    case_number = args.case
    case = await db.get_case_by_number(case_number)
    if not case:
        print(f"✗ תיק {case_number} לא נמצא")
        return 1
    chair = case.get("chair_name") or "דפנה תמיר"
    row = await _decision_law_row(case_number)

    # The 4 steps as durable nodes (X16 / INV-DUR1): each is checkpointed the
    # moment it finishes, so a crash/OOM in the long panel [3] resumes from [3]
    # instead of re-paying [0]–[2]. Steps [0] and [2] stay non-fatal (record the
    # error and continue); [1]/[3] may raise → the graph halts and the next run
    # resumes there. All steps are idempotent, so a fresh re-run is also safe.

    async def step_extract(results: dict) -> dict:
        # [0] extract the halachot the decision ITSELF states (its own case_law row).
        if not row:
            print(f"[0/4] ההחלטה {case_number} אינה ב-case_law עדיין — דילוג על חילוץ-הלכות")
            return {"extract": "skipped:not-enrolled"}
        if row.get("halacha_extraction_status") == "completed":
            print("[0/4] חילוץ-הלכות מההחלטה — דולג (כבר completed)")
            return {"extract": "skipped:completed"}
        if args.dry_run:
            print("[0/4] חילוץ-הלכות מההחלטה — מדולג (dry-run)")
            return {"extract": "skipped:dry-run"}
        print(f"[0/4] precedent_extract_halachot (החלטה {case_number})…", flush=True)
        try:
            raw0 = await precedent_extract_halachot(str(row["id"]))
            d0 = json.loads(raw0).get("data", {})
            print(f"  ✓ status={d0.get('status')} stored={d0.get('stored', d0.get('extracted'))}")
            return {"extract": d0.get("status", "done")}
        except Exception as e:  # non-fatal — record and continue
            print(f"  ⚠ halacha extraction failed (non-fatal): {e}")
            return {"extract": f"error:{e}"}

    async def step_citations(results: dict) -> dict:
        # [1] citation graph
        print(f"[1/4] extract_internal_citations (chair={chair})…", flush=True)
        raw = await extract_internal_citations(chair_name=chair, limit=0)
        try:
            d = json.loads(raw).get("data", {})
            print(f"  ✓ extracted {d.get('extracted')} · linked {d.get('linked')} "
                  f"· new {d.get('new')}")
            return {"citations": "done"}
        except Exception:
            print(f"  (citations returned: {str(raw)[:160]})")
            return {"citations": "unparsed"}

    async def step_corroboration(results: dict) -> dict:
        # [2] corroboration signal + policy (whole corpus backfill) — skip on dry-run.
        if args.dry_run:
            print("[2/4] corroboration_rebuild — מדולג (dry-run)")
            return {"corroboration": "skipped:dry-run"}
        print("[2/4] corroboration_rebuild (backfill)…", flush=True)
        try:
            cr = await corroboration.build_all()
            print(f"  ✓ {cr}")
            return {"corroboration": "done"}
        except Exception as e:  # non-fatal
            print(f"  ⚠ corroboration failed (non-fatal): {e}")
            return {"corroboration": f"error:{e}"}

    async def step_panel(results: dict) -> dict:
        # [3] three-judge halacha panel (the long step durability protects).
        apply = not args.dry_run
        print(f"[3/4] halacha_panel_approve {'--apply' if apply else '(dry-run)'} "
              f"(Opus+DeepSeek+Gemini)…", flush=True)
        import halacha_panel_approve as hpa
        # no_capture=False → persist panel rounds (FU-1, #133). Without this key
        # hpa.main() raises AttributeError at its capture step (the pipeline builds
        # the Namespace by hand, so every hpa arg must be set explicitly).
        rc = await hpa.main(Namespace(
            limit=args.limit, concurrency=6, apply=apply, no_capture=False))
        return {"panel_rc": rc or 0}

    steps = [
        _pipeline_runtime.Step("extract_decision_halachot", step_extract),
        _pipeline_runtime.Step("citations", step_citations),
        _pipeline_runtime.Step("corroboration", step_corroboration),
        _pipeline_runtime.Step("panel", step_panel),
    ]
    checkpoint_db = config.DATA_DIR / "checkpoints" / "halacha.sqlite"
    # Stable thread per case → an interrupted real run resumes; dry-runs are
    # previews (own thread, always fresh — never resume a stale preview).
    thread_id = f"halacha:{case_number}" + (":dryrun" if args.dry_run else "")
    results = await _pipeline_runtime.run_pipeline(
        steps,
        thread_id=thread_id,
        checkpoint_db=checkpoint_db,
        fresh=bool(args.fresh) or args.dry_run,
    )
    print("\n✓ pipeline-אימות-הלכות הושלם" + (" (dry-run)" if args.dry_run else ""))
    return int(results.get("panel_rc", 0) or 0)


if __name__ == "__main__":
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--case", required=True, help="case_number, e.g. 8126-03-25")
    ap.add_argument("--limit", type=int, default=0,
                    help="cap pending halachot judged (0 = full queue)")
    ap.add_argument("--dry-run", dest="dry_run", action="store_true",
                    help="citations only; skip corroboration writes; panel in dry-run")
    ap.add_argument("--fresh", action="store_true",
                    help="ignore any incomplete checkpoint and run from step [0] "
                         "(default: auto-resume an interrupted run; X16/INV-DUR1)")
    raise SystemExit(asyncio.run(main(ap.parse_args())))
