#!/usr/bin/env python3
"""Hook Dispatcher - Routes Claude Code hooks to HookManager.

This is a thin wrapper script that receives hook calls from Claude Code
and routes them to the appropriate handler via HookManager.

Usage:
    hook_dispatcher.py --type session-start < input.json > output.json
    hook_dispatcher.py --type pre-tool-use --debug < input.json > output.json

Exit Codes:
    0 - Success
    1 - General error (logged, continues)
    2 - Invalid input (argument parsing or JSON)
"""

import argparse
import asyncio
import json
import os
import sys
from pathlib import Path

import aiofiles

# No longer need to import HookManager - we call it via HTTP daemon instead

# Default daemon configuration
DEFAULT_DAEMON_PORT = 60887
DEFAULT_CONFIG_PATH = "~/.gobby/config.yaml"


_cached_daemon_url: str | None = None


async def get_daemon_url() -> str:
    """Get the daemon HTTP URL from config file.

    Reads daemon_port from ~/.gobby/config.yaml if it exists,
    otherwise uses the default port 60887. Result is cached
    for the lifetime of the process to avoid redundant file I/O.

    Returns:
        Full daemon URL like http://localhost:60887
    """
    global _cached_daemon_url
    if _cached_daemon_url is not None:
        return _cached_daemon_url

    config_path = Path(DEFAULT_CONFIG_PATH).expanduser()

    if config_path.exists():
        try:
            import yaml

            async with aiofiles.open(config_path, encoding="utf-8") as f:
                content = await f.read()
            config = yaml.safe_load(content) or {}
            port = config.get("daemon_port", DEFAULT_DAEMON_PORT)
        except Exception:
            # If config read fails, use default
            port = DEFAULT_DAEMON_PORT
    else:
        port = DEFAULT_DAEMON_PORT

    _cached_daemon_url = f"http://localhost:{port}"
    return _cached_daemon_url


def get_terminal_context() -> dict[str, str | int | None]:
    """Capture terminal/process context for session correlation.

    Returns:
        Dict with terminal identifiers (values may be None if unavailable)
    """
    context: dict[str, str | int | None] = {}

    # Parent process ID (shell or Claude process)
    try:
        context["parent_pid"] = os.getppid()
    except Exception:
        context["parent_pid"] = None

    # TTY device name
    try:
        context["tty"] = os.ttyname(0)
    except Exception:
        context["tty"] = None

    # macOS Terminal.app session ID
    context["term_session_id"] = os.environ.get("TERM_SESSION_ID")

    # iTerm2 session ID
    context["iterm_session_id"] = os.environ.get("ITERM_SESSION_ID")

    # VS Code terminal ID (if running in VS Code integrated terminal)
    context["vscode_terminal_id"] = os.environ.get("VSCODE_GIT_ASKPASS_NODE")

    # Tmux pane (only if actually running INSIDE a tmux session)
    # IMPORTANT: Only report TMUX_PANE if TMUX env var is also set.
    # The TMUX_PANE env var can be inherited by child processes that are
    # spawned into different terminals (e.g., Ghostty), which would cause
    # kill_agent to kill the parent's tmux pane instead of the child's terminal.
    if os.environ.get("TMUX"):
        context["tmux_pane"] = os.environ.get("TMUX_PANE")
    else:
        context["tmux_pane"] = None

    # Kitty terminal window ID
    context["kitty_window_id"] = os.environ.get("KITTY_WINDOW_ID")

    # Alacritty IPC socket path (unique per instance)
    context["alacritty_socket"] = os.environ.get("ALACRITTY_SOCKET")

    # Generic terminal program identifier (set by many terminals)
    context["term_program"] = os.environ.get("TERM_PROGRAM")

    # Gobby agent context (set by spawn_executor for terminal-mode agents)
    context["gobby_session_id"] = os.environ.get("GOBBY_SESSION_ID")
    context["gobby_parent_session_id"] = os.environ.get("GOBBY_PARENT_SESSION_ID")
    context["gobby_agent_run_id"] = os.environ.get("GOBBY_AGENT_RUN_ID")
    context["gobby_project_id"] = os.environ.get("GOBBY_PROJECT_ID")
    context["gobby_workflow_name"] = os.environ.get("GOBBY_WORKFLOW_NAME")

    return context


def parse_arguments() -> argparse.Namespace:
    """Parse command line arguments.

    Returns:
        Parsed arguments with type and debug flags
    """
    parser = argparse.ArgumentParser(description="Claude Code Hook Dispatcher")
    parser.add_argument(
        "--type",
        required=True,
        help="Hook type (e.g., session-start, pre-tool-use)",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        help="Enable debug logging",
    )
    return parser.parse_args()


async def check_daemon_running(timeout: float = 0.5) -> bool:
    """Check if gobby daemon is active and responding.

    Performs a quick health check to verify the HTTP server is running
    before processing hooks. This prevents hook execution when the daemon
    is stopped, avoiding long timeouts and confusing error messages.

    Args:
        timeout: Maximum time to wait for response in seconds (default: 0.5)

    Returns:
        True if client is running and responding, False otherwise
    """
    try:
        import httpx

        daemon_url = await get_daemon_url()
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{daemon_url}/admin/status",
                timeout=timeout,
                follow_redirects=False,
            )
        return response.status_code == 200
    except Exception:
        # Any error (connection refused, timeout, etc.) means client is not running
        return False


async def main() -> int:
    """Main dispatcher execution.

    Returns:
        Exit code (0=success, 1=error, 2=invalid input)
    """
    try:
        # Parse arguments
        args = parse_arguments()
    except (argparse.ArgumentError, SystemExit):
        # Argument parsing failed - return empty dict and exit 2
        print(json.dumps({}))
        return 2

    hook_type = args.type
    debug_mode = args.debug

    # Check if gobby daemon is running before processing hooks
    if not await check_daemon_running():
        # Critical hooks that manage session state MUST have daemon running
        # Without daemon, we lose handoff context, session tracking, etc.
        critical_hooks = {"session-start", "session-end", "pre-compact"}
        if hook_type in critical_hooks:
            # Block the hook - forces user to start daemon before critical lifecycle events
            print(
                f"\nGobby daemon is not running. Start with 'gobby start' before continuing. "
                f"({hook_type} requires daemon for session state management)",
                file=sys.stderr,
            )
            return 2  # Exit 2 = block operation
        else:
            # Non-critical hooks can proceed without daemon (tool use, notifications, etc.)
            print(
                json.dumps(
                    {"status": "daemon_not_running", "message": "gobby daemon is not running"}
                )
            )
            return 0  # Exit 0 (success) - allow operation to continue

    # Setup logger for dispatcher (not HookManager)
    # Only log to stderr in debug mode - otherwise logs pollute Claude's stderr reading
    import logging

    logger = logging.getLogger("gobby.hooks.dispatcher")
    if debug_mode:
        logging.basicConfig(level=logging.DEBUG)
    else:
        # In non-debug mode, suppress all logging to stderr
        logging.basicConfig(level=logging.WARNING, handlers=[])

    try:
        # Read JSON input from stdin asynchronously
        loop = asyncio.get_running_loop()
        raw = await loop.run_in_executor(None, sys.stdin.read)
        input_data = json.loads(raw)

        # Inject terminal context for session-start hooks
        # This captures the terminal/process info for session correlation
        if hook_type == "session-start":
            input_data["terminal_context"] = get_terminal_context()

        # ALWAYS log what Claude Code sends us (for debugging hook data issues)
        logger.info(f"[{hook_type}] Received input keys: {list(input_data.keys())}")

        # Log hook-specific critical fields (based on Claude Code SDK documentation)
        if hook_type == "session-start":
            logger.info(
                f"[session-start] session_id={input_data.get('session_id')}, "
                f"source={input_data.get('source')}"
            )
        elif hook_type == "session-end":
            logger.info(
                f"[session-end] session_id={input_data.get('session_id')}, "
                f"reason={input_data.get('reason')}"
            )
        elif hook_type == "user-prompt-submit":
            prompt = input_data.get("prompt", "")
            prompt_preview = prompt[:100] + "..." if len(prompt) > 100 else prompt
            logger.info(
                f"[user-prompt-submit] session_id={input_data.get('session_id')}, "
                f"prompt={prompt_preview}"
            )
        elif hook_type == "pre-tool-use":
            tool_input = input_data.get("tool_input", {})
            # Truncate large values for readability (keep first 200 chars)
            tool_input_preview = {
                k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
                for k, v in tool_input.items()
            }
            logger.info(
                f"[pre-tool-use] tool_name={input_data.get('tool_name')}, "
                f"tool_input={tool_input_preview}, "
                f"session_id={input_data.get('session_id')}"
            )
        elif hook_type == "post-tool-use":
            logger.info(
                f"[post-tool-use] tool_name={input_data.get('tool_name')}, "
                f"has_tool_response={bool(input_data.get('tool_response'))}, "
                f"has_tool_input={bool(input_data.get('tool_input'))}, "
                f"session_id={input_data.get('session_id')}"
            )
        elif hook_type == "pre-compact":
            logger.info(
                f"[pre-compact] session_id={input_data.get('session_id')}, "
                f"trigger={input_data.get('trigger')}, "
                f"has_custom_instructions={bool(input_data.get('custom_instructions'))}"
            )
        elif hook_type == "stop":
            logger.info(
                f"[stop] session_id={input_data.get('session_id')}, "
                f"stop_hook_active={input_data.get('stop_hook_active')}"
            )
        elif hook_type == "subagent-start":
            logger.info(
                f"[subagent-start] session_id={input_data.get('session_id')}, "
                f"agent_id={input_data.get('agent_id')}, "
                f"subagent_id={input_data.get('subagent_id')}"
            )
        elif hook_type == "subagent-stop":
            logger.info(
                f"[subagent-stop] session_id={input_data.get('session_id')}, "
                f"agent_id={input_data.get('agent_id')}, "
                f"subagent_id={input_data.get('subagent_id')}"
            )
        elif hook_type == "notification":
            logger.info(
                f"[notification] session_id={input_data.get('session_id')}, "
                f"message={input_data.get('message')}, "
                f"title={input_data.get('title', 'N/A')}"
            )
        elif hook_type == "permission-request":
            tool_input = input_data.get("tool_input", {})
            tool_input_preview = {
                k: (v[:200] + "..." if isinstance(v, str) and len(v) > 200 else v)
                for k, v in tool_input.items()
            }
            logger.info(
                f"[permission-request] tool_name={input_data.get('tool_name')}, "
                f"tool_input={tool_input_preview}, "
                f"session_id={input_data.get('session_id')}"
            )

        if debug_mode:
            logger.debug(f"Input data: {input_data}")

    except json.JSONDecodeError as e:
        # Invalid JSON input - return empty dict and exit 2
        if debug_mode:
            logger.error(f"JSON decode error: {e}")
        print(json.dumps({}))
        return 2

    # Call daemon HTTP endpoint instead of creating HookManager
    import httpx

    daemon_url = await get_daemon_url()
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{daemon_url}/hooks/execute",
                json={
                    "hook_type": hook_type,
                    "input_data": input_data,
                    "source": "claude",  # Required: identifies CLI source
                },
                timeout=90.0,  # LLM-powered hooks (pre-compact summary) need more time
            )

        if response.status_code == 200:
            # Success - daemon returns result directly (not wrapped)
            result = response.json()

            if debug_mode:
                logger.debug(f"Output data: {result}")

            # Check for block decision - return exit code 2 to signal blocking
            # For blocking, output goes to STDERR (Claude reads stderr on exit 2)
            if result.get("continue") is False or result.get("decision") == "block":
                # Output just the reason, not the full JSON
                reason = result.get("stopReason") or result.get("reason") or "Blocked by hook"
                print(f"\n{reason.rstrip()}", file=sys.stderr)
                return 2

            # Only print output if there's something meaningful to show
            # Empty dicts cause Claude Code to show "hook success: Success"
            if result and result != {}:
                print(json.dumps(result))

            return 0
        else:
            # HTTP error from daemon
            error_detail = response.text
            logger.error(
                f"Daemon returned error: status={response.status_code}, detail={error_detail}"
            )
            print(json.dumps({"status": "error", "message": f"Daemon error: {error_detail}"}))
            return 1

    except httpx.ConnectError:
        # Daemon not reachable - this shouldn't happen since we checked, but handle gracefully
        logger.error("Failed to connect to daemon (unreachable)")
        print(json.dumps({"status": "error", "message": "Daemon unreachable"}))
        return 1

    except httpx.TimeoutException:
        # Hook processing took too long
        logger.error(f"Hook execution timeout: {hook_type}")
        print(json.dumps({"status": "error", "message": "Hook execution timeout"}))
        return 1

    except Exception as e:
        # General error - log and return 1
        logger.error(f"Hook execution failed: {e}", exc_info=True)
        print(json.dumps({"status": "error", "message": str(e)}))
        return 1


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