"""
Hooks System

Lifecycle hooks for extending agent behavior.

Hook types:
- PreToolUse: Validate/transform before tool execution
- PostToolUse: React after tool execution (format, lint, log)
- SessionStart: Initialize session state
- SessionEnd: Persist state, evaluate session
- Stop: Final checks after each response

Inspired by Claude Code's hooks system.
"""

import json
import subprocess
from pathlib import Path
from typing import Any, Callable, Optional

from rich.console import Console

console = Console()


class HookType:
    PRE_TOOL_USE = "PreToolUse"
    POST_TOOL_USE = "PostToolUse"
    SESSION_START = "SessionStart"
    SESSION_END = "SessionEnd"
    STOP = "Stop"


class HookResult:
    """Result from running a hook."""
    
    def __init__(self, allowed: bool = True, message: str = "", modified_args: dict = None):
        self.allowed = allowed
        self.message = message
        self.modified_args = modified_args


class HookRunner:
    """
    Manages and executes lifecycle hooks.
    
    Hooks are Python callables or shell commands that run at
    specific lifecycle points. They can:
    - Block operations (PreToolUse)
    - Modify arguments (PreToolUse)
    - Run side effects (PostToolUse)
    - Log/persist state (SessionStart/End)
    """
    
    def __init__(self, project_root: Optional[Path] = None):
        self.project_root = project_root
        self._hooks: dict[str, list[dict]] = {
            HookType.PRE_TOOL_USE: [],
            HookType.POST_TOOL_USE: [],
            HookType.SESSION_START: [],
            HookType.SESSION_END: [],
            HookType.STOP: [],
        }
        self._python_hooks: dict[str, list[Callable]] = {
            HookType.PRE_TOOL_USE: [],
            HookType.POST_TOOL_USE: [],
            HookType.SESSION_START: [],
            HookType.SESSION_END: [],
            HookType.STOP: [],
        }
    
    def register_python_hook(self, hook_type: str, callback: Callable) -> None:
        """Register a Python function as a hook."""
        if hook_type in self._python_hooks:
            self._python_hooks[hook_type].append(callback)
    
    def load_from_config(self, config_path: Path) -> None:
        """Load hooks from a JSON configuration file."""
        if not config_path.exists():
            return
        
        try:
            config = json.loads(config_path.read_text(encoding="utf-8"))
            hooks_config = config.get("hooks", {})
            
            for hook_type, hook_list in hooks_config.items():
                if hook_type in self._hooks:
                    self._hooks[hook_type].extend(hook_list)
        except Exception as e:
            console.print(f"[dim]Warning: Failed to load hooks config: {e}[/dim]")
    
    def run_pre_tool_use(self, tool_name: str, arguments: dict) -> HookResult:
        """Run PreToolUse hooks. Can block or modify tool execution."""
        context = {
            "hook_type": HookType.PRE_TOOL_USE,
            "tool_name": tool_name,
            "tool_input": arguments,
        }
        
        # Run Python hooks
        for hook_fn in self._python_hooks[HookType.PRE_TOOL_USE]:
            try:
                result = hook_fn(context)
                if result and not result.get("allowed", True):
                    return HookResult(
                        allowed=False,
                        message=result.get("message", "Blocked by hook"),
                    )
                if result and "modified_args" in result:
                    arguments.update(result["modified_args"])
            except Exception:
                pass
        
        # Run command hooks
        for hook_def in self._hooks[HookType.PRE_TOOL_USE]:
            if not self._matches(hook_def.get("matcher", "*"), tool_name, arguments):
                continue
            
            for hook in hook_def.get("hooks", []):
                result = self._run_command_hook(hook, context)
                if result and not result.allowed:
                    return result
        
        return HookResult(allowed=True, modified_args=arguments)
    
    def run_post_tool_use(self, tool_name: str, arguments: dict, output: str) -> None:
        """Run PostToolUse hooks. For side effects only."""
        context = {
            "hook_type": HookType.POST_TOOL_USE,
            "tool_name": tool_name,
            "tool_input": arguments,
            "tool_output": {"output": output},
        }
        
        for hook_fn in self._python_hooks[HookType.POST_TOOL_USE]:
            try:
                hook_fn(context)
            except Exception:
                pass
        
        for hook_def in self._hooks[HookType.POST_TOOL_USE]:
            if not self._matches(hook_def.get("matcher", "*"), tool_name, arguments):
                continue
            for hook in hook_def.get("hooks", []):
                self._run_command_hook(hook, context)
    
    def run_session_hooks(self, hook_type: str) -> None:
        """Run SessionStart, SessionEnd, or Stop hooks."""
        context = {"hook_type": hook_type}
        
        for hook_fn in self._python_hooks.get(hook_type, []):
            try:
                hook_fn(context)
            except Exception:
                pass
        
        for hook_def in self._hooks.get(hook_type, []):
            for hook in hook_def.get("hooks", []):
                self._run_command_hook(hook, context)
    
    def _matches(self, matcher: str, tool_name: str, arguments: dict) -> bool:
        """Check if a hook matcher matches the current context."""
        if matcher == "*":
            return True
        
        # Simple matching: check if tool name is in matcher
        if f'tool == "{tool_name}"' in matcher or f"tool == '{tool_name}'" in matcher:
            return True
        
        # Check for command patterns
        if "tool_input.command" in matcher and "command" in arguments:
            cmd = arguments["command"]
            # Extract pattern from "matches" clause
            import re
            matches_pattern = re.search(r'matches\s+"([^"]+)"', matcher)
            if matches_pattern:
                pattern = matches_pattern.group(1)
                if re.search(pattern, cmd):
                    return True
        
        return False
    
    def _run_command_hook(self, hook: dict, context: dict) -> Optional[HookResult]:
        """Execute a command-type hook."""
        if hook.get("type") != "command":
            return None
        
        command = hook.get("command", "")
        if not command:
            return None
        
        timeout = hook.get("timeout", 30)
        is_async = hook.get("async", False)
        
        try:
            input_data = json.dumps(context)
            
            process = subprocess.run(
                command,
                shell=True,
                input=input_data,
                capture_output=True,
                text=True,
                timeout=timeout if not is_async else 1,
                cwd=str(self.project_root) if self.project_root else None,
            )
            
            # Check stderr for hook messages
            if process.stderr:
                for line in process.stderr.strip().split("\n"):
                    if "[Hook]" in line:
                        console.print(f"[dim]{line}[/dim]")
                        if "BLOCKED" in line:
                            return HookResult(allowed=False, message=line)
            
            return HookResult(allowed=process.returncode == 0)
            
        except subprocess.TimeoutExpired:
            return HookResult(allowed=True)
        except Exception:
            return HookResult(allowed=True)

    # --- Built-in Python hooks ---
    
    @staticmethod
    def dangerous_command_guard(context: dict) -> dict:
        """Built-in hook to block dangerous commands."""
        if context.get("tool_name") != "run_command":
            return {"allowed": True}
        
        cmd = context.get("tool_input", {}).get("command", "")
        dangerous = [
            "rm -rf /", "rm -rf ~", "format c:", "del /s /q c:\\",
            ":(){:|:&};:", "mkfs.",
        ]
        
        for pattern in dangerous:
            if pattern in cmd.lower():
                return {
                    "allowed": False,
                    "message": f"BLOCKED: Potentially dangerous command detected",
                }
        
        return {"allowed": True}
    
    @staticmethod
    def log_tool_usage(context: dict) -> dict:
        """Built-in hook to log all tool usage."""
        tool = context.get("tool_name", "unknown")
        
        # Log for debugging (could write to file)
        if context.get("hook_type") == HookType.POST_TOOL_USE:
            output = context.get("tool_output", {}).get("output", "")
            # Only flag actual tool errors (output starts with "Error:"),
            # not general output that happens to contain the word.
            if output.startswith("Error:"):
                console.print(f"  [dim red]! {tool} encountered an error[/dim red]")
        
        return {"allowed": True}


def create_default_hooks(project_root: Optional[Path] = None) -> HookRunner:
    """Create a HookRunner with sensible defaults."""
    runner = HookRunner(project_root)
    
    # Register built-in safety hooks
    runner.register_python_hook(
        HookType.PRE_TOOL_USE,
        HookRunner.dangerous_command_guard,
    )
    runner.register_python_hook(
        HookType.POST_TOOL_USE,
        HookRunner.log_tool_usage,
    )
    
    # Load project-specific hooks
    if project_root:
        hooks_config = project_root / ".ai-coder" / "hooks.json"
        runner.load_from_config(hooks_config)
    
    return runner
