#!/usr/bin/env python3
"""
Small local regression fleet + firmware READ CDB CPU sweep for the GDR-8050L live iSCSI target.

This is intentionally a local/offline gate over a captured live JSONL artifact.
It does not replace Windows/VLC/unlockdrive testing, but it lets each update be
checked against the known-good v206/v227 behavior before packaging.
"""
from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Any

REPORT_SPECS = [
    ("trace", "hlds_gdr_iscsi_trace_report.py", "_trace_report"),
    ("handoff", "hlds_gdr_firmware_handoff_report.py", "_firmware_handoff_report"),
    ("shadow_3e", "hlds_gdr_firmware_shadow_3e_report.py", "_firmware_shadow_3e_report"),
    ("shadow_capacity", "hlds_gdr_firmware_shadow_capacity_report.py", "_firmware_shadow_capacity_report"),
    ("shadow_bounds", "hlds_gdr_firmware_shadow_bounds_report.py", "_firmware_shadow_bounds_report"),
    ("shadow_lba_map", "hlds_gdr_firmware_shadow_lba_map_report.py", "_firmware_shadow_lba_map_report"),
]


def _load_json(path: Path) -> dict[str, Any]:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception as e:
        return {"_load_error": str(e), "_path": str(path)}


def _safe_extract_zip(zpath: Path, outdir: Path) -> None:
    with zipfile.ZipFile(zpath, "r") as zf:
        for info in zf.infolist():
            raw_name = info.filename.replace("\\", "/")
            if raw_name.endswith("/"):
                continue
            parts = [p for p in raw_name.split("/") if p and p not in (".", "..")]
            if not parts:
                continue
            dest = outdir.joinpath(*parts)
            dest.parent.mkdir(parents=True, exist_ok=True)
            with zf.open(info, "r") as src, dest.open("wb") as dst:
                shutil.copyfileobj(src, dst)


def _find_jsonl(root: Path) -> Path:
    cands = list(root.rglob("*.jsonl"))
    if not cands:
        raise FileNotFoundError(f"no .jsonl files found under {root}")
    def score(p: Path) -> tuple[int, int, str]:
        name = p.name.lower()
        s = 0
        if "stateful_relock_keep_loaded" in name: s += 50
        if "start_live" in name: s += 25
        if "live" in name: s += 10
        if "requests" in name: s -= 100
        return (-s, -p.stat().st_size, str(p))
    cands.sort(key=score)
    return cands[0]


def _resp_dir_for(jsonl: Path) -> Path | None:
    p = jsonl.with_name(jsonl.stem + "_responses")
    return p if p.exists() and p.is_dir() else None


def _run(cmd: list[str], cwd: Path) -> tuple[int, str, str]:
    proc = subprocess.run(cmd, cwd=str(cwd), text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    return proc.returncode, proc.stdout, proc.stderr


def run_reports(jsonl: Path, resp_dir: Path | None, out_prefix: Path, *, python_exe: str, script_dir: Path) -> dict[str, Any]:
    out_prefix.parent.mkdir(parents=True, exist_ok=True)
    results: dict[str, Any] = {}
    trace_json_path: Path | None = None
    for key, script, suffix in REPORT_SPECS:
        script_path = script_dir / script
        if not script_path.exists():
            results[key] = {"ok": False, "missing_script": str(script_path)}
            continue
        jout = out_prefix.with_name(out_prefix.name + suffix + ".json")
        mout = out_prefix.with_name(out_prefix.name + suffix + ".md")
        cmd = [python_exe, str(script_path), str(jsonl), "--json", str(jout), "--md", str(mout)]
        if key == "handoff" and trace_json_path is not None:
            cmd += ["--trace-report-json", str(trace_json_path)]
        if key in ("trace", "shadow_capacity", "shadow_bounds", "shadow_lba_map") and resp_dir is not None:
            cmd += ["--response-dir", str(resp_dir)]
        rc, stdout, stderr = _run(cmd, cwd=script_dir)
        loaded = _load_json(jout) if jout.exists() else {}
        results[key] = {
            "ok": rc == 0 and not loaded.get("_load_error"),
            "returncode": rc,
            "json": str(jout),
            "md": str(mout),
            "stdout_tail": stdout.strip().splitlines()[-5:],
            "stderr_tail": stderr.strip().splitlines()[-10:],
            "report": loaded,
        }
        if key == "trace" and jout.exists():
            trace_json_path = jout
    return results


def _bool_path(d: dict[str, Any], *keys: str) -> bool:
    cur: Any = d
    for k in keys:
        if not isinstance(cur, dict) or k not in cur:
            return False
        cur = cur[k]
    return bool(cur)


def evaluate(reports: dict[str, Any], *, strict_zero_checks: bool = False) -> dict[str, Any]:
    trace = reports.get("trace", {}).get("report", {})
    shadow3e = reports.get("shadow_3e", {}).get("report", {})
    cap = reports.get("shadow_capacity", {}).get("report", {})
    bounds = reports.get("shadow_bounds", {}).get("report", {})
    lba = reports.get("shadow_lba_map", {}).get("report", {})

    cases: dict[str, dict[str, Any]] = {}
    cases["single_lun_attach"] = {
        "ok": _bool_path(trace, "regression", "single_lun_only") or bool(trace.get("regression", {}).get("single_lun_only")),
        "details": "REPORT LUNS/attach did not expose multiple good LUNs",
    }
    checks = int(trace.get("check_condition_count") or 0)
    unexpected = int(trace.get("unexpected_check_condition_count") or 0)
    expected_boundary = int(trace.get("expected_boundary_check_condition_count") or 0)
    cases["check_condition_policy"] = {
        "ok": (checks == 0 if strict_zero_checks else unexpected == 0),
        "details": f"checks={checks} unexpected={unexpected} expected_boundary={expected_boundary}",
    }
    cases["known_good_v206_flow"] = {
        "ok": bool(trace.get("known_good_v206_complete")),
        "details": "locked video -> auth unlock -> Xbox view flow observed",
    }
    cases["known_good_eject_relock_flow"] = {
        "ok": bool(trace.get("known_good_v206_eject_cycle_complete")),
        "details": "Explorer/software eject relock cycle observed",
    }
    observed_3e = int(shadow3e.get("observed_shadow_rows") or 0)
    auth_3e_complete = bool(shadow3e.get("auth_sequence_complete"))
    cases["firmware_shadow_3e"] = {
        "ok": bool(shadow3e.get("ok")) and observed_3e > 0 and auth_3e_complete and int(shadow3e.get("mismatch_count") or 0) == 0,
        "details": f"observed={shadow3e.get('observed_shadow_rows')} auth_complete={auth_3e_complete} mismatches={shadow3e.get('mismatch_count')}",
    }
    cases["firmware_shadow_capacity"] = {
        "ok": bool(cap.get("ok")) and bool(cap.get("capacity_sequence_complete")) and int(cap.get("mismatch_count") or 0) == 0,
        "details": f"read_capacity={cap.get('read_capacity_rows')} mismatches={cap.get('mismatch_count')}",
    }
    observed_bounds = int(bounds.get("observed_bounds_rows") or 0)
    cases["firmware_shadow_bounds"] = {
        "ok": bool(bounds.get("ok")) and observed_bounds > 0 and int(bounds.get("mismatch_count") or 0) == 0,
        "details": f"observed={bounds.get('observed_bounds_rows')} mismatches={bounds.get('mismatch_count')}",
    }
    cases["firmware_shadow_lba_map"] = {
        "ok": bool(lba.get("ok")) and int(lba.get("mismatch_count") or 0) == 0 and bool(lba.get("candidate_delta_equals_configured")),
        "details": f"candidate_delta={lba.get('candidate_delta_from_unlocked_bridge_rows')} configured={lba.get('configured_media_lba_delta')} firmware_derived={lba.get('firmware_derived')}",
    }

    failed = {k: v for k, v in cases.items() if not v.get("ok")}
    return {
        "ok": not failed,
        "case_count": len(cases),
        "failed_count": len(failed),
        "cases": cases,
        "failed_cases": failed,
        "summary": {
            "total_rows": trace.get("total_rows"),
            "scsi_command_rows": trace.get("scsi_command_rows"),
            "check_condition_count": checks,
            "unexpected_check_condition_count": unexpected,
            "expected_boundary_check_condition_count": expected_boundary,
            "candidate_delta": lba.get("candidate_delta_from_unlocked_bridge_rows"),
            "firmware_derived": lba.get("firmware_derived"),
        }
    }


def write_md(path: Path, result: dict[str, Any]) -> None:
    lines = []
    lines.append("# GDR-8050L local regression fleet + firmware READ CDB CPU sweep")
    lines.append("")
    lines.append(f"Input JSONL: `{result.get('jsonl')}`")
    if result.get('response_dir'):
        lines.append(f"Response dir: `{result.get('response_dir')}`")
    lines.append("")
    evalr = result.get("evaluation", {})
    lines.append(f"Overall OK: **{str(evalr.get('ok')).lower()}**")
    lines.append("")
    lines.append("## Summary")
    for k, v in (evalr.get("summary") or {}).items():
        lines.append(f"- `{k}`: `{v}`")
    lines.append("")
    lines.append("## Fleet cases")
    for name, case in (evalr.get("cases") or {}).items():
        mark = "PASS" if case.get("ok") else "FAIL"
        lines.append(f"- **{mark}** `{name}` — {case.get('details','')}")
    lines.append("")
    lines.append("## Report files")
    for key, rep in (result.get("reports") or {}).items():
        lines.append(f"- `{key}`: `{rep.get('md')}`")
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="Run the small offline GDR-8050L local regression fleet + firmware READ CDB CPU sweep over a captured live JSONL/artifact zip.")
    ap.add_argument("input", help="Captured live JSONL or artifact zip from collect_logs")
    ap.add_argument("--jsonl", help="Explicit JSONL inside/alongside the input")
    ap.add_argument("--response-dir", help="Explicit response directory")
    ap.add_argument("--out-prefix", default="local_fleet_v227", help="Output prefix for generated report files")
    ap.add_argument("--json", default="", help="Write aggregate JSON report")
    ap.add_argument("--md", default="", help="Write aggregate Markdown report")
    ap.add_argument("--keep-extract", action="store_true", help="Keep extracted temporary directory when input is a zip")
    ap.add_argument("--strict-zero-checks", action="store_true", help="Require no CHECK CONDITION rows at all, not just no unexpected rows")
    ap.add_argument("--python", default=sys.executable or "python")
    args = ap.parse_args(argv)

    script_dir = Path(__file__).resolve().parent
    inp = Path(args.input).resolve()
    tmp: tempfile.TemporaryDirectory[str] | None = None
    work_root = inp.parent
    extracted_dir: Path | None = None
    if inp.suffix.lower() == ".zip":
        if args.keep_extract:
            extracted_dir = inp.with_suffix("")
            if extracted_dir.exists():
                shutil.rmtree(extracted_dir)
            extracted_dir.mkdir(parents=True)
        else:
            tmp = tempfile.TemporaryDirectory(prefix="gdr_fleet_")
            extracted_dir = Path(tmp.name)
        _safe_extract_zip(inp, extracted_dir)
        work_root = extracted_dir

    if args.jsonl:
        jsonl = Path(args.jsonl).resolve()
    elif inp.suffix.lower() == '.jsonl':
        # Important: when the input itself is a JSONL, use it directly.
        # Earlier v216 builds scanned the parent directory and could pick a stale sibling trace.
        jsonl = inp
    else:
        jsonl = _find_jsonl(work_root)
    resp_dir = Path(args.response_dir).resolve() if args.response_dir else _resp_dir_for(jsonl)
    out_prefix = Path(args.out_prefix)
    if not out_prefix.is_absolute():
        out_prefix = Path.cwd() / out_prefix
    reports = run_reports(jsonl, resp_dir, out_prefix, python_exe=args.python, script_dir=script_dir)
    evaluation = evaluate(reports, strict_zero_checks=bool(args.strict_zero_checks))
    result = {
        "input": str(inp),
        "jsonl": str(jsonl),
        "response_dir": str(resp_dir) if resp_dir else "",
        "extracted_dir": str(extracted_dir) if extracted_dir else "",
        "reports": reports,
        "evaluation": evaluation,
    }

    json_out = Path(args.json) if args.json else out_prefix.with_name(out_prefix.name + "_local_fleet_report.json")
    md_out = Path(args.md) if args.md else out_prefix.with_name(out_prefix.name + "_local_fleet_report.md")
    json_out.write_text(json.dumps(result, indent=2, sort_keys=True), encoding="utf-8")
    write_md(md_out, result)
    print(f"[LOCAL-FLEET] cases={evaluation['case_count']} failed={evaluation['failed_count']} ok={str(evaluation['ok']).lower()}")
    print(f"[LOCAL-FLEET] json={json_out}")
    print(f"[LOCAL-FLEET] md={md_out}")
    if tmp is not None:
        tmp.cleanup()
    return 0 if evaluation.get("ok") else 2


if __name__ == "__main__":
    raise SystemExit(main())
