"""Supervise a captured child run with stall-guard's progress detector.

Fails OPEN: any problem loading or running the detector leaves the child
completely untouched. A watchdog must never be able to break the tool it guards.
"""

from __future__ import annotations

import importlib.machinery
import importlib.util
import json
import os
import subprocess
import sys
import threading
import time
from pathlib import Path

STALL_GUARD = Path(__file__).resolve().parents[1] / "monitor/bin/stall-guard"
STALL_EXIT = 91
DEFAULT_IDLE_WINDOW = 1800.0
DEFAULT_OUTPUT_CEILING = 3600.0
# p99.9 of 5918 measured cdx runs is 175m; past that, warn but never kill —
# a run still emitting novel output is indistinguishable from real progress.
DEFAULT_WALL_NOTICE = 10800.0
SAMPLE_INTERVAL = 15.0
KILL_GRACE = 10.0


def _load_detector():
    try:
        loader = importlib.machinery.SourceFileLoader("stall_guard", str(STALL_GUARD))
        spec = importlib.util.spec_from_loader("stall_guard", loader)
        module = importlib.util.module_from_spec(spec)
        loader.exec_module(module)
        return module
    except Exception:
        return None


def _env_float(name, default):
    try:
        return float(os.environ[name])
    except (KeyError, ValueError):
        return default


def supervise(argv, env, log_path, key="cdx"):
    """Run argv capturing into log_path, killing it loudly if it stalls.

    Returns the child's exit status, or STALL_EXIT when killed for stalling.
    """
    detector = _load_detector()
    idle_window = _env_float("CDX_STALL_IDLE_WINDOW", DEFAULT_IDLE_WINDOW)
    output_ceiling = _env_float("CDX_STALL_OUTPUT_CEILING", DEFAULT_OUTPUT_CEILING)
    wall_notice = _env_float("CDX_STALL_WALL_NOTICE", DEFAULT_WALL_NOTICE)
    enforce = os.environ.get("CDX_STALL_ENFORCE", "1") != "0"
    heartbeat = os.environ.get("CDX_STALL_HEARTBEAT")
    trace_dir = Path.home() / ".cache/stall-guard"

    with open(log_path, "wb") as log:
        child = subprocess.Popen(
            argv, env=env, stdin=subprocess.DEVNULL, stdout=log,
            stderr=subprocess.STDOUT, start_new_session=True,
        )

    if detector is None:
        sys.stderr.write(
            f"cdx: stall-guard DEGRADED — detector not loadable at {STALL_GUARD}, "
            f"this run is UNGUARDED\n"
        )
        sys.stderr.flush()
        return child.wait()

    try:
        trace_dir.mkdir(parents=True, exist_ok=True)
        trace_path = trace_dir / f"{key}-{int(time.time())}-{child.pid}.jsonl"
    except OSError:
        trace_path = None

    samples = []
    verdict = {}
    noticed = []
    stop = threading.Event()
    started = time.monotonic()

    def watch():
        nonlocal trace_path
        while not stop.wait(SAMPLE_INTERVAL):
            try:
                cpu, io_bytes = detector.sample_tree(child.pid)
                sample = {
                    "t": time.monotonic() - started,
                    "out": detector.size_of(str(log_path)),
                    "cpu": cpu, "io": io_bytes,
                    "beat": detector.mtime_of(heartbeat) if heartbeat else 0.0,
                }
                samples.append(sample)
            except Exception as exc:
                sys.stderr.write(
                    f"cdx: stall-guard DEGRADED — sampling failed, this run is now "
                    f"UNGUARDED: {exc!r}\n"
                )
                try:
                    detector.notify(f"stall-guard degraded: {key}", f"sampling failed: {exc!r}")
                except Exception:
                    pass
                return
            if wall_notice and sample["t"] > wall_notice and not noticed:
                noticed.append(sample["t"])
                notice = (
                    f"{key} still running after {sample['t'] / 60:.0f}m — slower than 99.9% "
                    f"of past runs, but its progress signals are moving. Not a stall; check it."
                )
                sys.stderr.write(f"cdx: SLOW — {notice}\n")
                sys.stderr.flush()
                try:
                    detector.notify(f"cdx unusually slow: {key}", notice)
                except Exception:
                    pass

            if trace_path is not None:
                try:
                    with open(trace_path, "a") as handle:
                        handle.write(json.dumps(sample) + "\n")
                except OSError as exc:
                    sys.stderr.write(f"cdx: stall-guard trace write failed, continuing: {exc!r}\n")
                    trace_path = None
            try:
                reason = detector.classify(samples, idle_window, output_ceiling)
            except Exception as exc:
                sys.stderr.write(
                    f"cdx: stall-guard DEGRADED — detector raised, this run is now "
                    f"UNGUARDED: {exc!r}\n"
                )
                return
            if not reason or verdict:
                continue
            verdict["reason"] = reason
            try:
                detector.dump_diagnostics(sys.stderr, samples, str(log_path), reason, child.pid)
                detector.notify(f"cdx stalled: {key}", reason)
            except Exception:
                pass
            if not enforce:
                sys.stderr.write(f"cdx: stall-guard observe mode — would have failed: {reason}\n")
                return
            try:
                detector.kill_tree(child.pid, KILL_GRACE)
            except Exception as exc:
                sys.stderr.write(f"cdx: stall-guard failed to kill the stalled tree: {exc!r}\n")
            return

    watcher = threading.Thread(target=watch, daemon=True)
    watcher.start()
    status = child.wait()
    stop.set()
    watcher.join(timeout=20)

    if verdict and enforce:
        sys.stderr.write(f"cdx: STALL — killed after {verdict['reason']}. Log: {log_path}\n")
        return STALL_EXIT
    return status
