#!/usr/bin/env python3
"""
aircontext — wrapper around `claude` providing automatic context compaction loop.

Usage:
    aircontext [args passed through to claude]

Behaviour:
    1. Verify ./AirContext/ exists and config.yaml is valid (create from templates if missing).
    2. Spawn `claude` as a child process (stdio inherited so UX matches running claude directly).
    3. A watcher thread polls AirContext/state.json; when compaction_ready=true, it:
         - terminates the running claude gracefully
         - re-spawns `claude --resume <session_id>` so the new isolated-summary chain takes effect
    4. Loop exits when the user closes claude without a pending compaction.
"""
from __future__ import annotations
import os
import sys
import signal
import subprocess
import threading
import time
from pathlib import Path

# Ensure plugin's scripts/ is importable regardless of how the wrapper is invoked.
_PLUGIN_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))

from core.auto_init import (  # noqa: E402
    auto_configure,
    propagate_settings_env,
    required_fields_missing,
)
from core.config_loader import ensure_aircontext_dir, validate_config  # noqa: E402
from core.state import StateFile  # noqa: E402


WATCH_INTERVAL_SECONDS = 2.0
TERMINATE_TIMEOUT_SECONDS = 10


def terminate_gracefully(proc: subprocess.Popen) -> None:
    """Send a platform-appropriate stop signal, then SIGKILL after timeout."""
    try:
        if os.name == "nt":
            proc.send_signal(signal.CTRL_BREAK_EVENT)
        else:
            proc.send_signal(signal.SIGTERM)
    except (ProcessLookupError, OSError):
        return
    try:
        proc.wait(timeout=TERMINATE_TIMEOUT_SECONDS)
    except subprocess.TimeoutExpired:
        proc.kill()
        try:
            proc.wait(timeout=5)
        except subprocess.TimeoutExpired:
            pass


def spawn_claude(resume_id: str | None, passthrough_args: list[str]) -> subprocess.Popen:
    cmd = ["claude"]
    if resume_id:
        cmd += ["--resume", resume_id]
    cmd += passthrough_args
    env = {**os.environ, "AIRCONTEXT_ACTIVE": "1"}
    creationflags = 0
    if os.name == "nt":
        creationflags = subprocess.CREATE_NEW_PROCESS_GROUP  # type: ignore[attr-defined]
    return subprocess.Popen(cmd, env=env, creationflags=creationflags)


def main() -> int:
    project_root = Path.cwd()
    air = project_root / "AirContext"

    # Make ~/.claude/settings.json `env` block visible in os.environ so the
    # validate_config call below resolves ${env:VAR} placeholders the same way
    # claude/hooks/compactor will at runtime.
    propagate_settings_env()

    created = ensure_aircontext_dir(air, plugin_root=_PLUGIN_ROOT)
    config_path = air / "config.yaml"
    if created:
        cfg = auto_configure(config_path)
        window = cfg.get("trigger", {}).get("model_context_window", 200000)
        print(
            f"[aircontext] AirContext/ initialised at {air}",
            file=sys.stderr,
        )
        print(
            f"[aircontext] Auto-configured: model_context_window={window}, "
            f"backend.type={cfg.get('backend', {}).get('type')}",
            file=sys.stderr,
        )

    missing = required_fields_missing(config_path)
    if missing:
        print(
            "[aircontext] Please fill these REQUIRED fields in "
            f"{config_path}:",
            file=sys.stderr,
        )
        for m in missing:
            print(f"[aircontext]   - {m}", file=sys.stderr)
        print(
            "[aircontext] Then re-run `aircontext` and you'll go straight into claude.",
            file=sys.stderr,
        )
        return 1

    err = validate_config(config_path)
    if err:
        print(f"[aircontext] Invalid config: {err}", file=sys.stderr)
        return 1

    state = StateFile(air / "state.json")
    state.reset_for_new_session()

    passthrough = sys.argv[1:]
    resume_id = state.last_session_id

    while True:
        proc = spawn_claude(resume_id, passthrough)
        pending_resume_id: list[str | None] = [None]
        stop_watcher = threading.Event()

        def watcher() -> None:
            while not stop_watcher.is_set():
                snap = state.read()
                if snap.get("compaction_ready"):
                    rid = snap.get("pending_resume_session_id")
                    if rid:
                        pending_resume_id[0] = rid
                        terminate_gracefully(proc)
                        return
                if stop_watcher.wait(WATCH_INTERVAL_SECONDS):
                    return

        t = threading.Thread(target=watcher, daemon=True)
        t.start()
        try:
            proc.wait()
        except KeyboardInterrupt:
            terminate_gracefully(proc)
        finally:
            stop_watcher.set()
            t.join(timeout=3)

        if pending_resume_id[0]:
            resume_id = pending_resume_id[0]
            state.clear_ready()
            print(
                f"[aircontext] Compaction applied, resuming session {resume_id}",
                file=sys.stderr,
            )
            continue

        # User exited without a pending compaction — finish.
        return proc.returncode or 0


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