#!/usr/bin/env python3
"""ASUCB Deliver Batch — batched-verb delivery fold for file-remediation Stage 4.

Plan §4 (docs/plan-remediation-workflow-v2.md), AC4/AC4-neg/AC4-neg2. Staged-work-rot
fix: one up-front verb authorizes scripts/push_remediated_file.py for every
gate-PASSING file in an in-session remediation batch, instead of a per-file push
prompt that gets forgotten (CLAUDE.md §Write Safety Guard step 6 — batched approval
covers N objects). This is a loop with gate-routing over the already-shipped per-file
writer; it adds no new write path and does not reimplement backup/verify.

Batch input (plan §4, REVISED): the in-session remediation set Claude just acted on,
NOT a ledger (§3's approval ledger is deferred — when it ships, its Approve/
Change-to-remediate rows become this manifest instead, same downstream flow).

Never retires: a manifest row with action=retire is a hard error for the whole run —
retire keeps its own verb gate (scripts/retire_file.py), never batch-covered.
"""

import argparse
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

sys.path.insert(0, os.path.dirname(__file__))
import push_remediated_file  # noqa: E402

CONFIRM_VERBS = push_remediated_file.CONFIRM_VERBS  # mirrored, not redefined


VALID_KINDS = ("pdf", "office")


def _gate_route(item):
    """Return (should_deliver, bucket, reason) for one manifest item.

    File type comes from the explicit `kind` field ("pdf" | "office") — NEVER
    inferred from which gate fields happen to be present, so a malformed or
    typeless row can never be silently reclassified and delivered without its
    proper gate (Codex review 2026-07-22, finding 1). needs_manual always wins.
    Office → appearance.ok; pdf → verapdf_compliant AND fidelity_identical.
    Anything unrecognized/malformed → skipped_malformed, never delivered.
    """
    if not isinstance(item, dict):
        return False, "skipped_malformed", "manifest row is not an object"
    if item.get("needs_manual"):
        return False, "skipped_gate_fail", "needs_manual"

    kind = item.get("kind")
    if kind == "office":
        appearance = item.get("appearance")
        if not isinstance(appearance, dict) or "ok" not in appearance:
            return False, "skipped_malformed", "office row missing appearance.ok"
        if appearance["ok"] is True:
            return True, None, None
        return False, "needs_individual_approval", "appearance-drift (office file changed visibly)"
    if kind == "pdf":
        gates = item.get("gates")
        if (not isinstance(gates, dict)
                or "verapdf_compliant" not in gates or "fidelity_identical" not in gates):
            return False, "skipped_malformed", "pdf row missing gates.verapdf_compliant/fidelity_identical"
        if gates["verapdf_compliant"] and gates["fidelity_identical"]:
            return True, None, None
        return False, "skipped_gate_fail", "gate-fail (veraPDF/fidelity)"
    return False, "skipped_malformed", f"missing/unknown kind {kind!r} (expected pdf|office)"


def deliver_batch(course_id, manifest, confirm_verb):
    """Route + deliver an in-session remediation batch through push_remediated_file.

    One verb authorizes every gate-passing item this call; gate-fail, needs_manual,
    appearance-drift, and retire rows are never covered by it (plan §4).
    """
    if confirm_verb not in CONFIRM_VERBS:
        return {"ok": False, "error":
                 f"--confirm-verb must be one of {list(CONFIRM_VERBS)}, got {confirm_verb!r}."}
    if not isinstance(manifest, list):
        return {"ok": False, "error": "manifest must be a JSON list of per-file records."}

    for item in manifest:
        if isinstance(item, dict) and item.get("action") == "retire":
            return {"ok": False, "error":
                     f"Manifest row file_id={item.get('file_id')!r} requests action=retire; "
                     "deliver_batch never retires — use retire_file.py's own verb gate."}

    ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    approved_via = f"batched-verb:{confirm_verb}@{ts}"

    delivered, skipped_gate_fail, needs_individual_approval = [], [], []
    skipped_malformed, failed = [], []

    for item in manifest:
        _d = item if isinstance(item, dict) else {}
        file_id = _d.get("file_id")
        display_name = _d.get("display_name")
        should_deliver, bucket, reason = _gate_route(item)

        if not should_deliver:
            entry = {"file_id": file_id, "display_name": display_name, "reason": reason}
            {"needs_individual_approval": needs_individual_approval,
             "skipped_malformed": skipped_malformed,
             "skipped_gate_fail": skipped_gate_fail}[bucket].append(entry)
            continue

        push_result = push_remediated_file.push_remediated_file(
            course_id, file_id, item.get("local_path"), confirm_verb)

        if push_result.get("ok"):
            delivered.append({
                "file_id": file_id, "display_name": display_name,
                "approved_via": approved_via,
                "canvas_url": push_result.get("canvas_url"),
                "backup_path": push_result.get("backup_path"),
            })
        else:
            failed.append({
                "file_id": file_id, "display_name": display_name,
                "error": push_result.get("error"),
                "backup_path": push_result.get("backup_path"),
            })

    totals = {
        "manifest_count": len(manifest),
        "delivered": len(delivered),
        "skipped_gate_fail": len(skipped_gate_fail),
        "needs_individual_approval": len(needs_individual_approval),
        "skipped_malformed": len(skipped_malformed),
        "failed": len(failed),
    }

    return {
        # gate-fail skips are normal (ok); failed deliveries and MALFORMED input
        # are not — surface both with a nonzero signal so the caller investigates.
        "ok": not failed and not skipped_malformed,
        "course_id": str(course_id),
        "approved_via": approved_via,
        "delivered": delivered,
        "skipped_gate_fail": skipped_gate_fail,
        "needs_individual_approval": needs_individual_approval,
        "skipped_malformed": skipped_malformed,
        "failed": failed,
        "totals": totals,
    }


def main():
    parser = argparse.ArgumentParser(
        description="Deliver an in-session remediation batch to Canvas: one verb "
                     "authorizes push_remediated_file.py for every gate-passing file.")
    parser.add_argument("--course-id", required=True)
    parser.add_argument("--manifest", required=True,
                        help="Path to batch.json (list of per-file remediation-result records)")
    parser.add_argument("--confirm-verb", required=True,
                        help="Explicit verb (push|approved|deploy) authorizing the whole "
                             "batch (CLAUDE.md Write Safety Guard step 6). Invalid verb is "
                             "refused by the function guard as ok=False/exit 1, not argparse.")
    parser.add_argument("--json", action="store_true", help="Emit JSON only")
    args = parser.parse_args()

    try:
        manifest = json.loads(Path(args.manifest).read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        result = {"ok": False, "error": f"Could not read --manifest: {exc}"}
    else:
        result = deliver_batch(args.course_id, manifest, args.confirm_verb)

    if args.json:
        print(json.dumps(result, indent=2))
    elif result.get("ok"):
        t = result["totals"]
        print(f"Delivered {t['delivered']}/{t['manifest_count']}. "
              f"Skipped (gate-fail/needs_manual): {t['skipped_gate_fail']}. "
              f"Needs individual approval (appearance-drift): {t['needs_individual_approval']}.")
    else:
        print(f"ERROR: {result.get('error')}")
        for f in result.get("failed", []):
            print(f"  FAILED file_id={f['file_id']}: {f['error']}")

    if not result.get("ok"):
        sys.exit(1)


if __name__ == "__main__":
    main()
