#!/usr/bin/env python3
"""Lint the v2 Task26 runbooks against bindings, supersession, sealed pins, and semantic deadlock rules."""
from __future__ import annotations

import hashlib
import json
import os
import re
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
GP = BASE / "task26-golden-path-2e0894ea-v2.md"
RC = BASE / "task26-recovery-runbook-2e0894ea-v2.md"

CANDIDATE = "2e0894eac92bc396cc4723bf1f18ebc653b95018dd41574df435941c235da925"
WHEEL = "af4a9d0a1ffffb6eb7551c1d6dc2b32853ca6d024332a4f8f5702bbf992f141b"
PLAN = "7ace03c6dad33d2fc3ef223621cbca68a150fde8429932138e252fb8498ac582"
ACTOR = "8527916639"

PRESERVED = {
    "../../dualcoach-golden-path-contract.md": "32a379d855c6e5af978bd9886e3bf49c616c7c20f1c1d5f100c19d8adfc5eb4a",
    "../../dualcoach-recovery-runbook.md": "ae2f5f9046c06f8f0b42693be5aa0d9c31cada8024ae1e5ab8ba19a3cf10f6fc",
    "task26-golden-path-2e0894ea.md": "1f55b8f967c6564113574d41bf1c15ce977d35c38b5687f1623ba37f235a7bdb",
    "task26-recovery-runbook-2e0894ea.md": "f4f2d2347b4b68a080b9c437a813accbef8c4bcc471cbf21d67ffff31e56f89b",
}

PINS = {
    "controller": "bd051dda8666ce9e014ec79c58acd4d7b8df2c27fad01a7a78c76e46c3388baf",
    "contract": "4128cece087f3f4eb84c3917207299fae6bd1ac9971d7e4e8676549106bb7c20",
    "permission": "8a290b11cac5b6957c772366abe875c7f635b8a3e7956a665471ffaa90b6c495",
    "dry_run_receipt": "2fc323f8b00c18f21598f717fb6a19a7e342b7786909e59adcf4bc3955bcef99",
    "readiness_receipt": "776e90301a63dce8912bf8a4110cd037f833c3a0e875a3ddd1289c0c9ee2ac9e",
    "archive_verifier": "12b97aa72e2df86719dbb7b80b477237d590aaaea81a9f812f321a1566936528",
    "archive_inventory": "29db87f2855c4804dbbc69244e68d9fb9f51808f7f7759fb592ed68f5f090185",
    "verifier_seal": "28ff72fdb7925cbdcd8e7dd7e8c058bcca42cab3377c34078164766197ed6968",
    "verification_receipt": "597a37e4912d729b3b5f6022bccff6dc12a73b024128805980f5ab1ade4fb83d",
    "verifier_tests": "1b92ac7250d1af013204d8074176b971307211eddee91606d4cdd0fcd05efd79",
    "verifier_input": "cd9e080ab886d503a205e7a7335eb23942b161daeaa2028a6e8f63492a99ba68",
    "candidate_verifier": "b852482350129dd025f0c4c79539e04a71c4758c9ec336e2c5bcda44676791f6",
    "unit_file": "0b46e887fc12c45f12814f4ef025c10101ed36e5df0ac35f70abb1862c0c1db2",
    "config": "f93106b16643227e2ef9dec67a5bbd497e1d353e779da62287898a087071af87",
}

results: list[dict] = []


def check(name: str, ok: bool, detail: str = "") -> None:
    results.append({"check": name, "status": "PASS" if ok else "FAIL", "detail": detail})


def flow(text: str) -> str:
    return re.sub(r"\s+", " ", text)


def main() -> int:
    gp = GP.read_text(encoding="utf-8")
    rc = RC.read_text(encoding="utf-8")
    fgp, frc = flow(gp), flow(rc)
    both = fgp + " " + frc

    for path, label in ((GP, "golden path v2"), (RC, "recovery v2")):
        check(f"exists:{label}", path.is_file())
        check(f"mode600:{label}", oct(os.stat(path).st_mode & 0o777) == "0o600")

    for rel, want in PRESERVED.items():
        p = (BASE / rel).resolve()
        got = hashlib.sha256(p.read_bytes()).hexdigest() if p.is_file() else "missing"
        check(f"preserved:{p.name}", got == want, got[:16])

    for name, pin in [("candidate", CANDIDATE), ("wheel", WHEEL), ("plan", PLAN), ("actor", ACTOR)]:
        check(f"binding:gp:{name}", pin in gp)
        check(f"binding:rc:{name}", pin in rc)

    for sha in list(PRESERVED.values()):
        check(f"supersession-cites:{sha[:8]}", sha in gp and sha in rc)

    for name, pin in PINS.items():
        check(f"pin:gp:{name}", pin in gp or pin[:16] in fgp)
        check(f"pin:rc:{name}", pin in rc or pin[:16] in frc or name in {"verifier_input", "candidate_verifier", "unit_file", "config"})

    # --- semantic deadlock / exactness checks ---
    g8 = next((ln for ln in gp.splitlines() if ln.startswith("| G8 |")), "")
    check("deadlock:G8-no-holder-required", "no active lock holder" in g8 or "No active holder" in g8)
    check("deadlock:G8-no-absence-demand", "absent" not in g8.lower())
    check("deadlock:G8-disposal-via-reset", "approved_clear_scopes" in g8)
    g15 = next((ln for ln in gp.splitlines() if ln.startswith("| G15 |")), "")
    check("deadlock:G15-lock-absent", bool(g15) and "absent" in g15.lower() and "gateway.lock" in g15)
    check("deadlock:hand-removal-forbidden", "Hand removal is forbidden" in fgp and "Never remove the lock by hand" in frc)

    check("order:dry-execute-verify", "dry-run, then execute, then verify" in both)
    check("order:no-stale-mode-order", "dry-run, verify, execute" not in both and "dry-run (PASS required), verify" not in both)

    check("cleanup:B6-named-open", "B6" in fgp and "OPEN" in fgp and "nonterminal" in fgp)
    check("cleanup:R16-present", "R16" in frc and "nonterminal bootstrap authority" in frc)
    check("cleanup:no-terminal-forcing", "do not force a transition" in fgp.lower() or "Do not edit the ledger" in fgp)

    check("approval:literal-phrase", "TASK26_ARCHIVE_FIRST_PROFILE_RESET_APPROVED" in both)
    check("approval:not-a-receipt-path", "one-use approval receipt" not in both)

    check("verifier:input-file", "verifier-input.json" in fgp and "verifier-input.json" in frc)
    check("verifier:exit0", "Exit code `0`" in fgp or "exit `0`" in frc)
    check("verifier:no-exit2-claim", "exit 2" not in both.lower() and "exit code 2" not in both.lower())

    check("ledger:start-work-path", ".omo/start-work/ledger.jsonl" in both)
    check("ledger:no-profile-ledger-authority", "telegram-customer-bootstrap-v1/ledger.jsonl" not in both)

    check("archive-root:new-archives", "data/profile-reset-archives" in both)
    check("archive-root:prior-archives", "data/rehearsal-reset-archives" in both)
    check("archive-root:pending-rename", ".pending-" in both)

    check("blockers:B2-closed", "B2" in fgp and "CLOSED" in fgp)
    check("blockers:B5-closed", "B5" in fgp and "CLOSED" in fgp)
    check("blockers:B3-open", "B3" in both and "OPEN" in both)
    check("blockers:G12-open", "G12" in fgp and re.search(r"G12[^\n]*OPEN", fgp) is not None)
    check("stale:no-not-yet-sealed", "not yet sealed" not in both.lower() and "not been sealed" not in both.lower())

    pres = re.findall(r"preserv\w*[^.\n]{0,120}", both, re.I)
    bad_pres = [p for p in pres if re.search(r"\bsessions\b|\bstate\.db\b", p) and "archive" not in p and "receipt" not in p and "evidence" not in p]
    check("stale:no-sessions-preserved-claim", not bad_pres, json.dumps(bad_pres[:1]))

    check("gates:G1-G16", all(re.search(rf"\| G{i} \|", fgp) for i in range(1, 17)))
    check("procedures:R1-R16", all(re.search(rf"\bR{i}\b", frc) for i in range(1, 17)))
    check("sections:gp", all(f"## {i}." in gp for i in range(0, 15)))

    check("dualcoach-admin:void-only-in-rc", "dualcoach_admin invite" not in fgp)
    check("dualcoach-admin:void-declared", "NON-EXISTENT" in frc and "provider-auth check" in frc)

    for value in ("120", "90", "300", "600", "60"):
        check(f"deadline:{value}s", re.search(rf"\b{value} seconds\b", both) is not None)

    forbidden_ids = ["task22_dm_rehearsal", "task26_synthetic_rehearsal", "task26_same_actor_rehearsal",
                     "cb_2NQV5sbkN-M6awycJH7X5g", "cb_6S6RABpDgZ165V02A7qEzw", "cb_9yTz0oNwdU8s8hradru8BA",
                     "cb_rmDnfrqA6gkmjoQEdwxu0g", "wizard_995f04a3b8bc256fa13ff407", "3f44a18ea620d963"]
    check("forbidden-ids:listed", all(i in fgp for i in forbidden_ids))
    check("run-ids:one-use", "task26-live-reset-2e0894ea-cleanup" in fgp and "one-use" in both)

    for doc, label in ((gp, "gp"), (rc, "rc")):
        check(f"style:{label}:no-em-dash", "\u2014" not in doc and "\u2013" not in doc)
        slop = [w for w in ("leverage", "utilize", "robust", "seamless", "comprehensive", "streamline",
                            "facilitate", "empower", "cutting-edge", "best-in-class") if re.search(rf"\b{w}\b", doc, re.I)]
        check(f"style:{label}:no-slop", not slop, json.dumps(slop))

    matrix_rows = len(re.findall(r"^\| [A-Z]", rc, re.M))
    check("matrix:rows>=14", matrix_rows >= 14, str(matrix_rows))

    failed = [r for r in results if r["status"] == "FAIL"]
    summary = {"schema": "task26-runbook-lint-results-v2", "total": len(results),
               "passed": len(results) - len(failed), "failed": len(failed),
               "status": "PASS" if not failed else "FAIL", "results": results}
    out = BASE / "lint-results-v2.json"
    out.write_text(json.dumps(summary, indent=1) + "\n", encoding="utf-8")
    os.chmod(out, 0o600)
    print(json.dumps({k: summary[k] for k in ("total", "passed", "failed", "status")}))
    for r in failed:
        print("FAIL:", r["check"], r["detail"][:200])
    return 0 if not failed else 1


if __name__ == "__main__":
    sys.exit(main())
