#!/usr/bin/env python3
"""Run configured PRD Plugin hook behavior in one Python process per event.

Host manifests call this dispatcher once. It reads the persistent configuration
before importing any behavior script, skips disabled work, and replays the same
hook payload to each enabled handler. Non-gating handlers remain fail-open.
"""

import argparse
import hashlib
import io
import json
import os
import runpy
import sys
from pathlib import Path
from types import SimpleNamespace


EVENTS = ("SessionStart", "UserPromptSubmit", "Stop", "PreToolUse", "PostToolUse")


def _dig(data, key, default=False):
    current = data
    for part in key.split("."):
        if not isinstance(current, dict) or part not in current:
            return default
        current = current[part]
    return current


def _on(config, *keys):
    return all(_dig(config, key) is True for key in keys)


def enabled_handlers(config, event, host):
    """Return ordered handler filenames after all cheap config gates."""
    if not _on(config, "hooks.enabled"):
        return []
    guard = ([] if _dig(config, "reasoning_guard.mode", "report") == "off"
             else ["prd_reason_guard.py"])
    if event == "SessionStart":
        return guard + (["prd_nudge.py"] if _on(
            config, "hooks.nudge.enabled", "hooks.nudge.on_session_start") else [])
    if event == "UserPromptSubmit":
        return guard + (["prd_nudge.py"] if _on(
            config, "hooks.nudge.enabled", "hooks.nudge.on_user_prompt") else [])
    if event == "PreToolUse":
        handlers = list(guard)
        # Ordered cheapest-first: the scope guard only inspects the command
        # string and a git diff, so it never delays a commit the gate is about
        # to block anyway.
        if _on(config, "hooks.test_scope_guard.enabled"):
            handlers.append("prd_test_scope_guard.py")
        if _on(config, "hooks.precommit_gate.enabled", "automation.precommit_gate"):
            handlers.append("prd_precommit_gate.py")
        return handlers
    if event == "PostToolUse":
        return guard + (
            ["prd_log_skill.py"] if _on(config, "hooks.skill_log.enabled") else [])
    if event != "Stop":
        return []

    handlers = list(guard)
    if _on(config, "hooks.stop_guard.enabled", "automation.autonomous_run_until_done"):
        handlers.append("prd_stop_guard.py")
    if _on(config, "hooks.session_report.enabled"):
        handlers.append("prd_session_report.py")
    if _on(config, "hooks.drift_check.enabled", "drift.monitoring.enabled",
           "drift.monitoring.on_stop"):
        handlers.append("prd_drift_check.py")
    if host in {"codex", "opencode"} and _on(
            config, "hooks.archive_automation.enabled", "automation.archive_completed_sessions"):
        handlers.append("archive_automation_session.py")
    if _on(config, "hooks.reflection.enabled", "reflection.enabled", "reflection.on_stop"):
        handlers.append("prd_reflection.py")
    return handlers


def _load_config(root):
    try:
        data = json.loads((Path(root) / ".prd_plugin" / "config.json").read_text(
            encoding="utf-8-sig"))
        return data if isinstance(data, dict) else {}
    except (OSError, json.JSONDecodeError):
        return {}


def _payload_root(raw, explicit_root=None):
    if explicit_root:
        return Path(explicit_root).resolve()
    try:
        payload = json.loads(raw) if raw.strip() else {}
        if isinstance(payload, dict) and payload.get("cwd"):
            return Path(payload["cwd"]).resolve()
    except (json.JSONDecodeError, TypeError, ValueError):
        pass
    return Path.cwd().resolve()


def _handler_path(root, filename):
    if filename == "archive_automation_session.py":
        for candidate in (
                Path(root) / ".prd_plugin" / "scripts" / filename,
                Path(root) / "scripts" / filename):
            if candidate.is_file():
                return candidate
        return None
    candidate = Path(__file__).resolve().parent / filename
    return candidate if candidate.is_file() else None


def run_lifecycle_workflow(root, event, payload, config):
    """Run the configured lifecycle definition before specialized emitters."""
    if not _on(config, "hooks.enabled", "hooks.workflow.enabled", "workflows.enabled"):
        return None
    workflow_id = None
    if event == "SessionStart" and _on(config, "hooks.workflow.on_session_start"):
        workflow_id = "session.start"
    elif event == "Stop" and _on(config, "hooks.workflow.on_stop"):
        workflow_id = "session.stop"
    if workflow_id is None:
        return None
    scripts = next((candidate for candidate in (
        Path(root) / ".prd_plugin" / "scripts", Path(root) / "scripts")
        if (candidate / "prd_workflows.py").is_file()), None)
    if scripts is None:
        raise RuntimeError("prd_workflows.py is not installed")
    try:
        parsed = json.loads(payload) if payload.strip() else {}
    except json.JSONDecodeError:
        parsed = {}
    session = next((parsed.get(key) for key in ("session_id", "sessionId", "conversation_id")
                    if isinstance(parsed, dict) and parsed.get(key)), None)
    identity = str(session or hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16])
    sys.path.insert(0, str(scripts))
    try:
        import prd_workflows
        return prd_workflows.run_workflow(
            root, workflow_id, {}, idempotency_key=f"hook:{event}:{identity}")
    finally:
        try:
            sys.path.remove(str(scripts))
        except ValueError:
            pass


def run_handler(path, payload, root, host=None, event=None):
    """Execute one existing hook script in-process with isolated stdio/argv.

    ``host`` is exported as PRD_HOOK_HOST so handlers (the stop guard's
    cross-host goal filter, REQ-105) know which runtime they serve."""
    old_stdin, old_stdout, old_stderr = sys.stdin, sys.stdout, sys.stderr
    old_argv, old_cwd, old_path = sys.argv, Path.cwd(), list(sys.path)
    old_host = os.environ.get("PRD_HOOK_HOST")
    old_event = os.environ.get("PRD_HOOK_EVENT")
    stdout, stderr = io.StringIO(), io.StringIO()
    returncode = 0
    try:
        os.chdir(root)
        if host:
            os.environ["PRD_HOOK_HOST"] = str(host)
        if event:
            os.environ["PRD_HOOK_EVENT"] = str(event)
        sys.stdin, sys.stdout, sys.stderr = io.StringIO(payload), stdout, stderr
        sys.argv = [str(path)]
        try:
            runpy.run_path(str(path), run_name="__main__")
        except SystemExit as exc:
            value = exc.code
            returncode = value if isinstance(value, int) else (0 if value is None else 1)
    except Exception as exc:
        returncode = 1
        print(f"{type(exc).__name__}: {exc}", file=stderr)
    finally:
        sys.stdin, sys.stdout, sys.stderr = old_stdin, old_stdout, old_stderr
        sys.argv = old_argv
        sys.path[:] = old_path
        os.chdir(old_cwd)
        if old_host is None:
            os.environ.pop("PRD_HOOK_HOST", None)
        else:
            os.environ["PRD_HOOK_HOST"] = old_host
        if old_event is None:
            os.environ.pop("PRD_HOOK_EVENT", None)
        else:
            os.environ["PRD_HOOK_EVENT"] = old_event
    return SimpleNamespace(returncode=returncode, stdout=stdout.getvalue(),
                           stderr=stderr.getvalue())


def _block_decision(output):
    for line in reversed(output.splitlines()):
        try:
            value = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(value, dict) and value.get("decision") == "block":
            return value
    return None


def dispatch(root, event, host, payload, stdout=None, stderr=None):
    stdout = sys.stdout if stdout is None else stdout
    stderr = sys.stderr if stderr is None else stderr
    config = _load_config(root)
    try:
        workflow_run = run_lifecycle_workflow(root, event, payload, config)
        if workflow_run and workflow_run.get("status") not in {"completed", "waiting_judgment"}:
            message = (f"[PRD Plugin] lifecycle workflow {workflow_run.get('id')} "
                       f"{workflow_run.get('status')}: {workflow_run.get('error', {}).get('message', 'unknown error')}\n")
            stderr.write(message)
            if _on(config, "hooks.workflow.fail_closed"):
                stdout.write(json.dumps({"decision": "block", "reason": message.strip()}) + "\n")
                return 2 if event == "PreToolUse" else 0
    except Exception as exc:
        message = f"[PRD Plugin] lifecycle workflow unavailable: {type(exc).__name__}: {exc}\n"
        stderr.write(message)
        if _on(config, "hooks.workflow.fail_closed"):
            stdout.write(json.dumps({"decision": "block", "reason": message.strip()}) + "\n")
            return 2 if event == "PreToolUse" else 0
    for filename in enabled_handlers(config, event, host):
        path = _handler_path(root, filename)
        if path is None:
            continue
        result = run_handler(path, payload, root, host=host, event=event)
        if event == "PreToolUse" and result.returncode == 2:
            stdout.write(result.stdout)
            stderr.write(result.stderr)
            return 2
        # All other hook plumbing remains fail-open. Preserve useful context
        # output, and stop the chain when a Stop guard/reflection asks to block.
        stdout.write(result.stdout)
        if event == "Stop" and _block_decision(result.stdout):
            return 0
    return 0


def _settings_already_wire_prd(root):
    """True if the project's .claude/settings.json already fires this dispatcher.

    A plugin-native invocation defers to it so an installed repo (whose settings
    hooks use the install-time-detected interpreter) is never run twice.
    """
    settings = Path(root) / ".claude" / "settings.json"
    try:
        data = json.loads(settings.read_text(encoding="utf-8-sig"))
    except (OSError, ValueError):
        return False
    hooks = data.get("hooks")
    if not isinstance(hooks, dict):
        return False
    for groups in hooks.values():
        if not isinstance(groups, list):
            continue
        for group in groups:
            for hook in (group or {}).get("hooks", []) if isinstance(group, dict) else []:
                if "prd_hook_dispatch" in str((hook or {}).get("command", "")):
                    return True
    return False


def parse_args(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--event", required=True, choices=EVENTS)
    parser.add_argument("--host", choices=("codex", "claude", "opencode"), default="codex")
    parser.add_argument("--repo-root")
    # How this invocation reached the dispatcher. A plugin-native hook
    # (hooks/hooks.json, ${CLAUDE_PLUGIN_ROOT}) fires in EVERY enabled repo, but
    # a repo installed with a per-repo .claude/settings.json already fires the
    # same dispatcher. To avoid running twice, a `plugin` invocation defers when
    # the project's settings already wire PRD hooks (REQ-149).
    parser.add_argument("--source", choices=("settings", "plugin"), default="settings")
    return parser.parse_args(argv)


def main(argv=None):
    args = parse_args(argv)
    payload = sys.stdin.read()
    root = _payload_root(payload, args.repo_root)
    if args.source == "plugin" and _settings_already_wire_prd(root):
        # The repo's own settings hooks will handle this event; stay silent.
        return 0
    return dispatch(root, args.event, args.host, payload)


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