#!/usr/bin/env python3
"""Lint the Task26 v3 Golden Path and Recovery Runbook pair (runbook-rebind task st_01a0054e).

v3 linter: checks the v3 supersession pair (v1 and v2 runbooks are preserved byte-identical and are
checked by their own linters). Enforces structural, binding, and semantic rules, including the
deadlock-free reset/harness ordering, the one-invite/one-Start/one-delivery rules, the corrected
command set, and the closed-gate requirements for READY_FOR_REHEARSAL_PREFLIGHT.
"""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
GP = BASE / "task26-golden-path-2e0894ea-v3.md"
RC = BASE / "task26-recovery-runbook-2e0894ea-v3.md"
RECEIPT = BASE / "runbook-binding-receipt-v3.json"

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

# Every evidence pin cited in v3 (all independently re-verified at authoring).
PINS = [
    "b852482350129dd025f0c4c79539e04a71c4758c9ec336e2c5bcda44676791f6",  # candidate verifier
    "cd9e080ab886d503a205e7a7335eb23942b161daeaa2028a6e8f63492a99ba68",  # verifier-input.json
    "8278dde4efa8bcd366fce873ccc07f651818275238b67452b9e6cb93c67afd6d",  # provider raw receipt (B3)
    "6aeaaab42e0030b0f626037d7b9ef6661ec93fb57fd991c843967c1fc695905c",  # provider payload digest
    "16acf1b355715379ad526433803353e1f34c418e9560fc1466f3f77f951df919",  # provider receipt index
    "7d163cb01708869d617efeadd852f7e6ec4f963a94c50d19b104cc849483c171",  # profile snapshot
    "f9a46172386333a0067f43695fb1429a043a606f04baceb06e8c59b90c36235c",  # provider command digest
    "0e9b4b1f0449d8351602598fead3ef01b8616e230c147e44db79667284ceae49",  # dualcoach_admin module
    "2826b1baf97918829f80d41722ec6534f933e57efa52345353cdb366cac314fb",  # invite harness (G12)
    "c4e857894ebd8b2e96289ea70d7cdcdd38cdad1c72cc7b1c86f33a5d2de7d171",  # harness permission seal
    "c7fc477141259087e8e9dd6520b2066eda3bab20dc6e2f625942431b6830c187",  # harness approval receipt
    "c2a6393f2ccf67ab6872f83e03793af72ea21b7df2e616c7cc2256c566c6b2c0",  # harness verification receipt
    "68f6f72f773265a3e889b5ab2024425caa7eb1fd0cf4f612379fd7203293185b",  # harness sealed tests
    "23845bda574c085e16b6d316e5e84e54b3bc6db3a8803c6b3b93ed7c4b86b049",  # approval event digest
    "145515d5e110dcebb94fcaa554bcee29544b3a75dcfe058fae944ea3b70042b2",  # bootstrap module member
    "8b03fa714304b34a7f19dc9b077d8fbd54e7f7bc46232bf8d5bc65ae7b873ff5",  # cleanup controller (B6)
    "7b6a1aa54b58d6e7e6733a963772e220a903ffae5a3e0e6d089283af13e097db",  # cleanup contract v2
    "cb06310270868da5dfc3c1d292b0ccedd5f3021d0f211576449c96649fa4730b",  # cleanup permission v2
    "90fcb04be789fd519d6bb83f9ba659927721e9d272594de84329c42c3529ed18",  # cleanup test receipt
    "d8d818cdfd2a22b088064b216136bfdee050b096a5b7fdcc9fb6f5958312dd05",  # cleanup independent receipt
    "fab6322753cccca614e1ee83bf5221013185662a74c86f5d19563e38f0fc0d2b",  # cleanup readiness
    "769db2bd5c39e405176ecb438d05ad0f10c538ee53c6c7849e809123f91ecb88",  # cleanup live rejection
    "78d7474df421f26ec4d106fa378270f60e57178553c3423fad717698c0fd5b22",  # cleanup exact execute cmd
    "e6888d62915b4b57b22b2769ebf46a7a1ee57d0d4ad7c4d5c1502bffc895ebdf",  # cleanup exact verify cmd
    "bd051dda8666ce9e014ec79c58acd4d7b8df2c27fad01a7a78c76e46c3388baf",  # pre-reset controller
    "4128cece087f3f4eb84c3917207299fae6bd1ac9971d7e4e8676549106bb7c20",  # pre-reset contract
    "8a290b11cac5b6957c772366abe875c7f635b8a3e7956a665471ffaa90b6c495",  # pre-reset permission
    "2fc323f8b00c18f21598f717fb6a19a7e342b7786909e59adcf4bc3955bcef99",  # pre-reset dry-run receipt
    "776e90301a63dce8912bf8a4110cd037f833c3a0e875a3ddd1289c0c9ee2ac9e",  # pre-reset readiness
    "12b97aa72e2df86719dbb7b80b477237d590aaaea81a9f812f321a1566936528",  # archive verifier
    "29db87f2855c4804dbbc69244e68d9fb9f51808f7f7759fb592ed68f5f090185",  # archive inventory
    "28ff72fdb7925cbdcd8e7dd7e8c058bcca42cab3377c34078164766197ed6968",  # archive seal
    "597a37e4912d729b3b5f6022bccff6dc12a73b024128805980f5ab1ade4fb83d",  # archive receipt
    "d2879b128573a9d75a55958d2fa3e1a24be1636f0a7a20affe8f2ef9b94dfd33",  # manifest evidence digest
    "a8191eed20ca75abcb03c33ac0c0b915cd810249466e816c6a1a55ba154af188",  # gateway/config.py member
    "f5d51008f1e8c8ee102df930fa68c7945a3276a249187adc7b7f6ba4b1e8e134",  # gateway/run.py member
    "3eca06827bfd62a4f07498bf7fed54e3b2b47a9f62e1f5b81701d1453e88806e",  # cron/scheduler.py member
    "cc0e4b697633150f3626daa8cf5bb4825bc96243f1a3dfadb6c9f540cad9906c",  # bootstrap registration member
    "6ab725042cbed536d8a8f3faee29e431470617f21d83c795e19574a27fcd032f",  # nutrition onboarding member
    "1f55b8f967c6564113574d41bf1c15ce977d35c38b5687f1623ba37f235a7bdb",  # v1 GP (harness-bound)
    "f4f2d2347b4b68a080b9c437a813accbef8c4bcc471cbf21d67ffff31e56f89b",  # v1 RC (harness-bound)
    "633d5c25ce70cb9ab5d19c434122b14b46fb7108962ab6e453ce6ab3ede1848f",  # v2 GP
    "6024c26ccae3cad7fcf46a77bca8607d73293a92ddb3dcf8fa5a278361869f1d",  # v2 RC
    "32a379d855c6e5af978bd9886e3bf49c616c7c20f1c1d5f100c19d8adfc5eb4a",  # original golden path
    "ae2f5f9046c06f8f0b42693be5aa0d9c31cada8024ae1e5ab8ba19a3cf10f6fc",  # original recovery runbook
    "f93106b16643227e2ef9dec67a5bbd497e1d353e779da62287898a087071af87",  # config.yaml
    "0b46e887fc12c45f12814f4ef025c10101ed36e5df0ac35f70abb1862c0c1db2",  # unit file
    "28420d4aa8fc1ee1298c9fe93a69e47aa187c0d2777ca672fb603cfbebc569cf",  # gateway.lock
    "9eb1b5ae", "7004dac9", "0bd0afb9", "68de185a",  # authority snapshot digests (short forms)
]

# Stale phrases that must NOT appear as live instructions (v1/v2 errors corrected in v3).
STALE_PATTERNS = [
    r"python -m hermes_cli\.nutrition_readiness --customer",      # wrong module + flag (v1/v2)
    r"request-generation --customer",                              # invented generation CLI (v1/v2)
    r"record-generation",                                          # invented
    r"customer_admin --registry \S+ create",                       # non-existent subcommand invoked
    r"customer_admin --registry \S+ reset",                        # non-existent subcommand invoked
    r"customer_admin --registry \S+ delete",                       # non-existent subcommand invoked
    r"customer_admin --registry \S+ set-next-checkin",             # non-existent subcommand invoked
    r"disable <[^>]*> --reason",                                   # non-existent flag
    r"/home/cube/\.hermes/profiles/dualcoachtest/\.venv",          # profile-local venv (stale)
    r"/etc/systemd/system/hermes-gateway-dualcoachtest",           # wrong unit path
    r"dry-run, verify, execute",                                   # wrong mode order (v1)
    r"gateway/cron/scheduler\.py",                                 # non-existent wheel path
    r"telegram_customer_onboarding\.py",                           # non-existent wheel path
    r"eff71467",                                                   # stale module pin
    r"exit `?2`?,? stdout JSON `?status`? == `?PASS",              # wrong exit-code claim (v1)
]

results: list[dict] = []


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


def main() -> int:
    check("file-pair-exists", GP.exists() and RC.exists(), f"{GP.name}, {RC.name}")
    if not (GP.exists() and RC.exists()):
        return report()
    gp = GP.read_text(encoding="utf-8")
    rc = RC.read_text(encoding="utf-8")

    # --- binding ---------------------------------------------------------------
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-candidate-full-hash", CANDIDATE in text)
        check(f"{label}-wheel-hash", WHEEL in text)
        check(f"{label}-plan-hash", PLAN in text)
        check(f"{label}-actor-id", ACTOR in text)
        check(f"{label}-candidate-mentions>=8", text.count(CANDIDATE) + text.count("2e0894ea") >= 8,
              f"{text.count(CANDIDATE) + text.count('2e0894ea')}")
        check(f"{label}-version-marker", "v3" in text and "2026-08-15" in text)

    for pin in PINS:
        check(f"pin-{pin[:8]}", pin in gp or pin in rc, pin)

    # --- stale/incorrect content must be absent --------------------------------
    for pat in STALE_PATTERNS:
        for label, text in (("gp", gp), ("rc", rc)):
            m = re.search(pat, text)
            check(f"{label}-stale-absent:{pat[:36]}", m is None, m.group(0)[:80] if m else "")

    # --- stale candidate + forbidden reused identifiers -------------------------
    stale_candidate = "19ed0e6047f0a4d7650c47a7413ee0298a243f768a63bcb8fb9cffe44d140a1a"
    check("gp-stale-candidate-superseded", gp.count(stale_candidate) >= 1 and "supersede" in gp.lower())
    check("rc-stale-candidate-superseded", rc.count(stale_candidate) >= 1)
    forbidden_ids = ["task22_dm_rehearsal", "task26_synthetic_rehearsal", "task26_same_actor_rehearsal",
                     "cb_2NQV5sbkN-M6awycJH7X5g", "cb_6S6RABpDgZ165V02A7qEzw",
                     "cb_9yTz0oNwdU8s8hradru8BA", "cb_rmDnfrqA6gkmjoQEdwxu0g",
                     "wizard_995f04a3b8bc256fa13ff407", "3f44a18ea620d963"]
    for fid in forbidden_ids:
        check(f"gp-forbidden-id-{fid[:24]}", fid in gp)
    overlap = [f for f in forbidden_ids if f in rc]
    check("rc-forbidden-id-overlap-scan", not overlap, f"overlap: {overlap}")
    for word in ["<link>", "<token>", "<nonce>", "TBD", "placeholder", "FIXME"]:
        check(f"gp-no-placeholder-{word}", word not in gp)
        check(f"rc-no-placeholder-{word}", word not in rc)

    # --- structural -------------------------------------------------------------
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-heading", text.lstrip().startswith("# "))
        check(f"{label}-has-verdict-language", "PASS" in text and "FAIL" in text)
        check(f"{label}-private-evidence-modes", "600" in text and "700" in text)
    check("gp-evidence-pins-verifier-exit0", "Exit `0`" in gp and "verifier-input.json" in gp)
    check("gp-deadlines-table", "120 seconds" in gp and "600 seconds" in gp and "90 seconds" in gp)
    check("rc-supported-command-table", "| Interface | Supported command shape |" in rc)
    check("rc-void-interface-section", "Void interfaces" in rc)
    check("rc-abort-matrix", "Abort decision matrix" in rc)

    # --- semantic deadlock / correctness checks ---------------------------------
    # 1. G8 row: no active holder required, and must NOT demand lock absence pre-reset.
    g8_line = next((ln for ln in gp.splitlines() if ln.startswith("| G8 |")), "")
    check("gp-g8-row-found", bool(g8_line))
    check("gp-g8-no-absence-demand", "absent" not in g8_line.lower(), "pre-reset gate must not demand lock absence")
    check("gp-g8-holder-check", "flock" in g8_line and "no active holder" in g8_line.lower())
    check("gp-g8-scope-check", "approved_clear_scopes" in g8_line)
    # 2. G15 row: post-reset gate must require lock absence.
    g15_line = next((ln for ln in gp.splitlines() if ln.startswith("| G15 |")), "")
    check("gp-g15-row-found", bool(g15_line))
    check("gp-g15-lock-absent", "absent" in g15_line.lower() and "gateway.lock" in g15_line)
    # 3. No manual lock deletion anywhere (semantic, instruction-level).
    for label, text in (("gp", gp), ("rc", rc)):
        check(f"{label}-no-manual-lock-deletion", "manual `gateway.lock` deletion" in text
              and re.search(r"\brm [^\n]*gateway\.lock|delete (the )?lock file by hand", text) is None)
    check("rc-lock-disposal-rule", "Any other deletion of the lock file is forbidden" in rc
          and "sealed pre-reset" in rc)
    # 4. Ordering chain (Section 2 of GP): the mandated sequence in order.
    chain = ["Pre-reset arm", "Pre-reset execute and verify", "Invite harness `dry-run` and `verify`",
             "Exact-wheel deployment", "Observe before prepare/Start", "Lifecycle:",
             "Disable and service stop", "Cleanup execute and verify"]
    positions = [gp.find(c) for c in chain]
    check("gp-ordering-chain-complete", all(p > 0 for p in positions),
          f"positions: {positions}")
    check("gp-ordering-chain-ordered", positions == sorted(positions), f"positions: {positions}")
    # 5. Mode order statements correct.
    check("gp-mode-order-cleanup", "`dry-run` -> `execute` -> `verify`" in rc or "dry-run, execute, then verify" in gp)
    check("gp-approval-literal-phrase", "'TASK26_ARCHIVE_FIRST_PROFILE_RESET_APPROVED'" in gp)
    check("gp-cleanup-approval-phrase", "TASK26_POST_LIFECYCLE_ARCHIVE_CLEANUP_APPROVED" in gp and
          "TASK26_POST_LIFECYCLE_ARCHIVE_CLEANUP_APPROVED" in rc)
    # 6. One invite / one Start / one delivery (semantic singleton rules).
    check("gp-one-prepare", "invite_prepare_maximum` is 1" in gp or "one-prepare maximum" in rc)
    check("gp-one-start", "accepted_start_claim_maximum: 1" in gp)
    check("gp-one-delivery", "exactly one customer DM" in gp and "exactly one `adaptive_plan_delivered`" in gp)
    check("rc-no-recovery-shortcuts", "no recovery shortcut" in rc.lower().replace(",", "") or "recovery shortcut" in rc)
    check("rc-expire-mode", "invite_harness.py expire" in rc)
    # 7. Closed gates: no OPEN gate remains; all three former blockers marked closed.
    check("gp-no-open-gates", [m.start() for m in re.finditer(r"\bOPEN\b", gp)] ==
          [gp.find("No gate is OPEN") + len("No gate is ")], "only the no-open-gates statement may contain OPEN")
    check("gp-b3-closed", "B3, CLOSED" in gp or "(B3 closed)" in gp)
    check("gp-g12-closed", "B4, CLOSED" in gp or "(G12 closed)" in gp)
    check("gp-b6-closed", "B6, CLOSED" in gp or "(B6 closed)" in gp)
    check("gp-decision-complete", "No gate is OPEN" in gp)
    check("rc-matrix-decision-complete", "no row is blocked" in rc)
    # 8. Corrected CLI set present.
    check("gp-readiness-cli-correct", "checkin_cli.readiness_cli --profile-root" in gp and "--customer-key" in gp)
    check("gp-activate-cli-correct", "customer_admin --registry" in gp and "--checklist-evidence" in gp)
    check("gp-disable-cli-correct", "disable <customer_key>" in gp)
    check("gp-venv-correct", "/home/cube/projects/richard/hermes-agent/.venv" in gp)
    check("gp-unit-path-correct", "/home/cube/.config/systemd/user/hermes-gateway-dualcoachtest.service" in gp)
    check("gp-generation-event-driven", "no generation CLI" in gp)
    check("gp-hermes-home-binding", "HERMES_HOME=/home/cube/.hermes/profiles/dualcoachtest" in gp)
    check("gp-digest-domain-clarification", "different digest domain" in gp or "different domain" in gp)
    # 9. Exact sealed commands present (spot anchors).
    check("gp-pre-reset-command", "reset_controller.py' execute --profile" in gp.replace("\n", " "))
    check("gp-cleanup-commands", gp.count("cleanup_controller.py' ") >= 3 or
          ("cleanup_controller.py' dry-run" in gp and "cleanup_controller.py' verify" in gp))
    check("gp-harness-commands", "invite_harness.py\" dry-run" in gp or "invite_harness.py dry-run" in gp or
          "invite_harness.py\" verify" in gp or "\"$PY\" -B \"$D/invite_harness.py\" dry-run" in gp)

    # --- receipt (v3) ------------------------------------------------------------
    if RECEIPT.exists():
        try:
            data = json.loads(RECEIPT.read_text(encoding="utf-8"))
        except json.JSONDecodeError as exc:
            data = None
            check("receipt-valid-json", False, str(exc))
        if data is not None:
            check("receipt-valid-json", True)
            check("receipt-schema", data.get("schema") == "task26-runbook-binding-receipt-v3")
            check("receipt-candidate", data.get("candidate") == CANDIDATE)
            check("receipt-wheel", data.get("wheel_sha256") == WHEEL)
            check("receipt-plan", data.get("plan_sha256") == PLAN)
            check("receipt-actor", data.get("actor") == ACTOR)
            check("receipt-supersedes-v2", "633d5c25ce70cb9ab5d19c434122b14b46fb7108962ab6e453ce6ab3ede1848f"
                  in json.dumps(data))
            check("receipt-self-hash-present", bool(data.get("self_sha256")))
            check("receipt-verdict", data.get("verdict", {}).get("task_verdict") == "PASS")
            check("receipt-ready-token", data.get("verdict", {}).get("readiness_token") == "READY_FOR_REHEARSAL_PREFLIGHT")
            check("receipt-no-open-blockers", not data.get("verdict", {}).get("open_blockers"))
    else:
        check("receipt-present", True, "skip: receipt written after lint")

    return report()


def report() -> int:
    failed = [r for r in results if not r["ok"]]
    for r in results:
        if not r["ok"]:
            print(f"FAIL {r['check']}: {r['detail']}")
    out = BASE / "lint-results-v3.json"
    out.write_text(json.dumps({"checks": results, "total": len(results),
                               "failed": len(failed)}, indent=2) + "\n", encoding="utf-8")
    print(f"{'PASS' if not failed else 'FAIL'} ({len(results) - len(failed)}/{len(results)} checks) -> {out.name}")
    return 0 if not failed else 1


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