from __future__ import annotations

import argparse
import hashlib
import json
import os
import pathlib
import subprocess
import sys
import time


ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from reliability_core.stage_control_r10r3r2 import allocate_deadlines, load_deadline_calibration


def canonical_hash(value: object) -> str:
    raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return hashlib.sha256(raw).hexdigest()


def negative_deadline_calibration(mode: str) -> dict[str, object]:
    if mode not in {"timeout", "stall", "signal"}:
        raise ValueError(f"unsupported negative-control mode: {mode}")
    calibration = load_deadline_calibration()
    semantic_worker_seconds = 1.2 if mode == "timeout" else 2.0
    reviewer_seconds = semantic_worker_seconds + calibration.reserve_seconds * 3.0
    hierarchy = allocate_deadlines(reviewer_seconds, calibration)
    if not hierarchy.passed:
        raise RuntimeError("negative-control deadline hierarchy could not be calibrated")
    if mode == "timeout":
        heartbeat_stall_seconds = hierarchy.controller_seconds + max(
            calibration.finalization_reserve_seconds,
            time.get_clock_info("monotonic").resolution,
        )
    elif mode == "stall":
        heartbeat_stall_seconds = min(0.7, hierarchy.worker_seconds * 0.5)
    else:
        heartbeat_stall_seconds = min(2.0, hierarchy.worker_seconds)
    launcher_timeout_seconds = (
        hierarchy.reviewer_seconds
        + calibration.startup_p95_seconds
        + calibration.reserve_seconds
    )
    return {
        "calibration": calibration,
        "hierarchy": hierarchy,
        "heartbeat_stall_seconds": heartbeat_stall_seconds,
        "launcher_timeout_seconds": launcher_timeout_seconds,
    }


def run_one(output: pathlib.Path, mode: str) -> dict:
    run = output / mode; run.mkdir(parents=True, exist_ok=False)
    budget = negative_deadline_calibration(mode)
    calibration = budget["calibration"]
    hierarchy = budget["hierarchy"]
    stall = float(budget["heartbeat_stall_seconds"])
    command = run / "child_command.json"
    command.write_text(json.dumps([sys.executable, str(ROOT / "scripts" / "r10r2_hazard_child.py"), mode]), encoding="utf-8")
    event = run / "event_journal.jsonl"; record_path = run / "supervisor_record.json"
    run_id = "negative-" + mode + "-" + str(time.time_ns())
    challenge = hashlib.sha256(run_id.encode("utf-8")).hexdigest()
    environment = {
        **os.environ,
        "R10_R2_RUN_ID": run_id,
        "R10_R2_REVIEWER_CHALLENGE_SHA256": challenge,
        "R10_R2_EVENT_JOURNAL_PATH": str(event),
        "R10_R2_SUPERVISOR_RECORD_PATH": str(record_path),
        "R10_R2_REVIEWER_DEADLINE_SECONDS": str(hierarchy.reviewer_seconds),
        "R10_R3_DEADLINE_CALIBRATION_JSON": json.dumps(calibration.to_dict(), sort_keys=True),
    }
    if mode == "signal":
        environment["R10_R2_SELF_SIGNAL_AFTER_SECONDS"] = "0.4"
    completed = subprocess.run(
        [
            sys.executable, str(ROOT / "scripts" / "r10r2_supervisor.py"),
            "--deadline-seconds", str(hierarchy.supervisor_seconds),
            "--heartbeat-stall-seconds", str(stall),
            "--child-command-json", str(command),
        ],
        cwd=ROOT,
        env=environment,
        capture_output=True,
        timeout=float(budget["launcher_timeout_seconds"]),
        check=False,
    )
    (run / "stdout.bin").write_bytes(completed.stdout)
    (run / "stderr.bin").write_bytes(completed.stderr)
    record = json.loads(record_path.read_text(encoding="utf-8"))
    journal = [json.loads(line) for line in event.read_text(encoding="utf-8").splitlines() if line.strip()]
    record.update({
        "launcher_exit_code": completed.returncode,
        "stdout_sha256": hashlib.sha256(completed.stdout).hexdigest(),
        "stderr_sha256": hashlib.sha256(completed.stderr).hexdigest(),
        "journal_sha256_recomputed": canonical_hash(journal),
        "journal_hash_match": canonical_hash(journal) == record["event_journal_sha256"],
        "negative_deadline_calibration": {
            "deadline_hierarchy": hierarchy.to_dict(),
            "heartbeat_stall_seconds": stall,
            "launcher_timeout_seconds": budget["launcher_timeout_seconds"],
            "calibration": calibration.to_dict(),
        },
    })
    return record


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--output-dir", required=True, type=pathlib.Path)
    args = parser.parse_args()
    args.output_dir.mkdir(parents=True, exist_ok=False)
    records = {
        "timeout": run_one(args.output_dir, "timeout"),
        "stall": run_one(args.output_dir, "stall"),
        "signal": run_one(args.output_dir, "signal"),
    }
    for name, row in records.items():
        row["negative_control"] = name
    passed = (
        records["timeout"]["timed_out"] is True
        and records["stall"]["stall_detected"] is True
        and records["signal"]["signal_received"] is not None
        and all(
            row["exit_code"] != 0 and row["final_record_written"] is True
            and row["cleanup_passed"] is True and row["orphan_process_count"] == 0
            and row["zombie_process_count"] == 0 and row["journal_hash_match"] is True
            for row in records.values()
        )
    )
    payload = {"records": records, "passed": passed}
    (args.output_dir / "negative_execution_records.json").write_text(
        json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8"
    )
    print(json.dumps({"passed": passed, "modes": sorted(records)}, sort_keys=True))
    return 0 if passed else 2


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