#!/usr/bin/env python3
"""Event handlers for Claude Code hook handler.

This module provides individual event handlers for different types of
Claude Code hook events.

Supports Dependency Injection:
- Optional services can be passed via constructor
- Lazy loading fallback for services not provided
- Eliminates runtime imports inside methods
"""

import asyncio
import json
import os
import re
import subprocess  # nosec B404 - subprocess used for safe claude CLI version checking only
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

# Import _log helper to avoid stderr writes (which cause hook errors)
try:
    from .hook_handler import _log
except ImportError:
    # Fallback for direct execution
    def _log(message: str) -> None:
        """Fallback logger when hook_handler not available."""


# Import tool analysis with fallback for direct execution
try:
    # Try relative import first (when imported as module)
    from .tool_analysis import (
        assess_security_risk,
        calculate_duration,
        classify_tool_operation,
        extract_tool_parameters,
        extract_tool_results,
    )
except ImportError:
    # Fall back to direct import (when parent script is run directly)
    from tool_analysis import (
        assess_security_risk,
        calculate_duration,
        classify_tool_operation,
        extract_tool_parameters,
        extract_tool_results,
    )

# Import correlation manager with fallback for direct execution
# WHY at top level: Runtime relative imports fail with "no known parent package" error
try:
    from .correlation_manager import CorrelationManager
except ImportError:
    from correlation_manager import CorrelationManager

# Debug mode - MUST match hook_handler.py default (false) to prevent stderr writes
DEBUG = os.environ.get("CLAUDE_MPM_HOOK_DEBUG", "false").lower() == "true"

# Import constants for configuration
try:
    from claude_mpm.core.constants import TimeoutConfig
except ImportError:
    # Fallback values if constants module not available
    class TimeoutConfig:
        QUICK_TIMEOUT = 2.0


# ============================================================================
# Optional Dependencies - loaded once at module level for DI
# ============================================================================

# Log manager (for agent prompt logging)
_log_manager: Optional[Any] = None
_log_manager_loaded = False


def _get_log_manager() -> Optional[Any]:
    """Get log manager with lazy loading."""
    global _log_manager, _log_manager_loaded
    if not _log_manager_loaded:
        try:
            from claude_mpm.core.log_manager import get_log_manager

            _log_manager = get_log_manager()
        except ImportError:
            _log_manager = None
        _log_manager_loaded = True
    return _log_manager


# Config service (for autotodos configuration)
_config: Optional[Any] = None
_config_loaded = False


def _get_config() -> Optional[Any]:
    """Get Config with lazy loading."""
    global _config, _config_loaded
    if not _config_loaded:
        try:
            from claude_mpm.core.config import Config

            _config = Config()
        except ImportError:
            _config = None
        _config_loaded = True
    return _config


# Delegation detector (for anti-pattern detection)
_delegation_detector: Optional[Any] = None
_delegation_detector_loaded = False


def _get_delegation_detector_service() -> Optional[Any]:
    """Get delegation detector with lazy loading."""
    global _delegation_detector, _delegation_detector_loaded
    if not _delegation_detector_loaded:
        try:
            from claude_mpm.services.delegation_detector import get_delegation_detector

            _delegation_detector = get_delegation_detector()
        except ImportError:
            _delegation_detector = None
        _delegation_detector_loaded = True
    return _delegation_detector


# Event log (for PM violation logging)
_event_log: Optional[Any] = None
_event_log_loaded = False


def _get_event_log_service() -> Optional[Any]:
    """Get event log with lazy loading."""
    global _event_log, _event_log_loaded
    if not _event_log_loaded:
        try:
            from claude_mpm.services.event_log import get_event_log

            _event_log = get_event_log()
        except ImportError:
            _event_log = None
        _event_log_loaded = True
    return _event_log


class EventHandlers:
    """Collection of event handlers for different Claude Code hook events.

    Supports dependency injection for optional services:
    - log_manager: For agent prompt logging
    - config: For autotodos configuration
    - delegation_detector: For anti-pattern detection
    - event_log: For PM violation logging

    If services are not provided, they are loaded lazily on first use.
    """

    def __init__(
        self,
        hook_handler,
        *,
        log_manager: Optional[Any] = None,
        config: Optional[Any] = None,
        delegation_detector: Optional[Any] = None,
        event_log: Optional[Any] = None,
    ):
        """Initialize with reference to the main hook handler and optional services.

        Args:
            hook_handler: The main ClaudeHookHandler instance
            log_manager: Optional LogManager for agent prompt logging
            config: Optional Config for autotodos configuration
            delegation_detector: Optional DelegationDetector for anti-pattern detection
            event_log: Optional EventLog for PM violation logging
        """
        self.hook_handler = hook_handler

        # Store injected services (None means use lazy loading)
        self._log_manager = log_manager
        self._config = config
        self._delegation_detector = delegation_detector
        self._event_log = event_log

    @property
    def log_manager(self) -> Optional[Any]:
        """Get log manager (injected or lazy loaded)."""
        if self._log_manager is not None:
            return self._log_manager
        return _get_log_manager()

    @property
    def config(self) -> Optional[Any]:
        """Get config (injected or lazy loaded)."""
        if self._config is not None:
            return self._config
        return _get_config()

    @property
    def delegation_detector(self) -> Optional[Any]:
        """Get delegation detector (injected or lazy loaded)."""
        if self._delegation_detector is not None:
            return self._delegation_detector
        return _get_delegation_detector_service()

    @property
    def event_log(self) -> Optional[Any]:
        """Get event log (injected or lazy loaded)."""
        if self._event_log is not None:
            return self._event_log
        return _get_event_log_service()

    def handle_user_prompt_fast(self, event):
        """Handle user prompt with comprehensive data capture.

        WHY enhanced data capture:
        - Provides full context for debugging and monitoring
        - Captures prompt text, working directory, and session context
        - Enables better filtering and analysis in dashboard
        """
        prompt = event.get("prompt", "")

        # Skip /mpm commands to reduce noise unless debug is enabled
        if prompt.startswith("/mpm") and not DEBUG:
            return

        # Detect and save @alias for sticky project context
        self._save_project_alias_if_present(prompt)

        # Emit immediate acknowledgment for long-running command feedback
        project_name = (
            event.get("cwd", "").split("/")[-1] if event.get("cwd") else "unknown"
        )
        ack_data = {
            "prompt_preview": prompt[:80] + "..." if len(prompt) > 80 else prompt,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "status": "received",
            "project": project_name,
        }
        self.hook_handler._emit_socketio_event("", "command_acknowledged", ack_data)

        # Capture PM-level directive to persistent memory (non-blocking)
        self._capture_pm_directive(prompt, project_name)

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        # Extract comprehensive prompt data
        prompt_data = {
            "prompt_text": prompt,
            "prompt_preview": prompt[:200] if len(prompt) > 200 else prompt,
            "prompt_length": len(prompt),
            "session_id": event.get("session_id", ""),
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "is_command": prompt.startswith("/"),
            "contains_code": "```" in prompt
            or "python" in prompt.lower()
            or "javascript" in prompt.lower(),
            "urgency": (
                "high"
                if any(
                    word in prompt.lower()
                    for word in ["urgent", "error", "bug", "fix", "broken"]
                )
                else "normal"
            ),
        }

        # Store prompt for comprehensive response tracking if enabled
        try:
            rtm = getattr(self.hook_handler, "response_tracking_manager", None)
            if (
                rtm
                and getattr(rtm, "response_tracking_enabled", False)
                and getattr(rtm, "track_all_interactions", False)
            ):
                session_id = event.get("session_id", "")
                if session_id:
                    pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
                    pending_prompts[session_id] = {
                        "prompt": prompt,
                        "timestamp": datetime.now(timezone.utc).isoformat(),
                        "working_directory": working_dir,
                    }
                    if DEBUG:
                        _log(
                            f"Stored prompt for comprehensive tracking: session {session_id[:8]}..."
                        )
        except Exception:  # nosec B110
            # Response tracking is optional - silently continue if it fails
            pass

        # Record user message for auto-pause if active
        auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
        if auto_pause and auto_pause.is_pause_active():
            try:
                auto_pause.on_user_message(prompt)
            except Exception as e:
                if DEBUG:
                    _log(f"Auto-pause user message recording error: {e}")

        # Check for incoming messages (cross-project messaging)
        try:
            from claude_mpm.hooks import message_check_hook

            message_notification = message_check_hook()
            if message_notification:
                # Inject message notification into PM context
                # This will appear in the next system reminder
                prompt_data["message_notification"] = message_notification
                if DEBUG:
                    _log("Message notification added to prompt data")
        except Exception as e:
            if DEBUG:
                _log(f"Message check hook error: {e}")

        # Emit normalized event (namespace no longer needed with normalized events)
        self.hook_handler._emit_socketio_event("", "user_prompt", prompt_data)

    def handle_pre_tool_fast(self, event):
        """Handle pre-tool use with comprehensive data capture.

        WHY comprehensive capture:
        - Captures tool parameters for debugging and security analysis
        - Provides context about what Claude is about to do
        - Enables pattern analysis and security monitoring
        """
        # Enhanced debug logging for session correlation
        session_id = event.get("session_id", "")
        if DEBUG:
            _log(f"  - session_id: {session_id[:16] if session_id else 'None'}...")
            _log(f"  - event keys: {list(event.keys())}")

        tool_name = event.get("tool_name", "")
        tool_input = event.get("tool_input", {})

        # Generate unique tool call ID for correlation with post_tool event
        tool_call_id = str(uuid.uuid4())

        # Extract key parameters based on tool type
        tool_params = extract_tool_parameters(tool_name, tool_input)

        # Classify tool operation
        operation_type = classify_tool_operation(tool_name, tool_input)

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        timestamp = datetime.now(timezone.utc).isoformat()

        pre_tool_data = {
            "tool_name": tool_name,
            "operation_type": operation_type,
            "tool_parameters": tool_params,
            "session_id": event.get("session_id", ""),
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": timestamp,
            "parameter_count": len(tool_input) if isinstance(tool_input, dict) else 0,
            "is_file_operation": tool_name
            in ["Write", "Edit", "MultiEdit", "Read", "LS", "Glob"],
            "is_execution": tool_name in ["Bash", "NotebookEdit"],
            "is_delegation": tool_name == "Task",
            "security_risk": assess_security_risk(tool_name, tool_input),
            "correlation_id": tool_call_id,  # Add correlation_id for pre/post correlation
        }

        # Store tool_call_id using CorrelationManager for cross-process retrieval
        if session_id:
            CorrelationManager.store(session_id, tool_call_id, tool_name)
            if DEBUG:
                _log(
                    f"  - Generated tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}..."
                )

        # Add delegation-specific data if this is a Task tool
        if tool_name == "Task" and isinstance(tool_input, dict):
            self._handle_task_delegation(tool_input, pre_tool_data, session_id)

        # Record tool call for auto-pause if active
        auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
        if auto_pause and auto_pause.is_pause_active():
            try:
                auto_pause.on_tool_call(tool_name, tool_input)
            except Exception as e:
                if DEBUG:
                    _log(f"Auto-pause tool recording error: {e}")

        self.hook_handler._emit_socketio_event("", "pre_tool", pre_tool_data)

        # Handle TodoWrite specially - emit dedicated todo_updated event
        # WHY: Frontend expects todo_updated events for dashboard display
        # The broadcaster.todo_updated() method exists but was never called
        if tool_name == "TodoWrite" and tool_params.get("todos"):
            todo_data = {
                "todos": tool_params["todos"],
                "total_count": len(tool_params["todos"]),
                "session_id": session_id,
                "timestamp": timestamp,
            }
            self.hook_handler._emit_socketio_event("", "todo_updated", todo_data)
            if DEBUG:
                _log(
                    f"  - Emitted todo_updated event with {len(tool_params['todos'])} todos for session {session_id[:8]}..."
                )

    def _handle_task_delegation(
        self, tool_input: dict, pre_tool_data: dict, session_id: str
    ):
        """Handle Task delegation specific processing."""
        # Normalize agent type to handle capitalized names like "Research", "Engineer", etc.
        raw_agent_type = tool_input.get("subagent_type", "unknown")

        # Use AgentNameNormalizer if available, otherwise simple lowercase normalization
        try:
            from claude_mpm.core.agent_name_normalizer import AgentNameNormalizer

            normalizer = AgentNameNormalizer()
            # Convert to Task format (lowercase with hyphens)
            agent_type = (
                normalizer.to_task_format(raw_agent_type)
                if raw_agent_type != "unknown"
                else "unknown"
            )
        except ImportError:
            # Fallback to simple normalization
            agent_type = (
                raw_agent_type.lower().replace("_", "-")
                if raw_agent_type != "unknown"
                else "unknown"
            )

        pre_tool_data["delegation_details"] = {
            "agent_type": agent_type,
            "original_agent_type": raw_agent_type,  # Keep original for debugging
            "prompt": tool_input.get("prompt", ""),
            "description": tool_input.get("description", ""),
            "task_preview": (
                tool_input.get("prompt", "") or tool_input.get("description", "")
            )[:100],
        }

        # Track this delegation for SubagentStop correlation and response tracking
        if DEBUG:
            _log(f"  - session_id: {session_id[:16] if session_id else 'None'}...")
            _log(f"  - agent_type: {agent_type}")
            _log(f"  - raw_agent_type: {raw_agent_type}")

        if session_id and agent_type != "unknown":
            # Prepare request data for response tracking correlation
            request_data = {
                "prompt": tool_input.get("prompt", ""),
                "description": tool_input.get("description", ""),
                "agent_type": agent_type,
            }
            self.hook_handler._track_delegation(session_id, agent_type, request_data)

            if DEBUG:
                _log("  - Delegation tracked successfully")
                _log(f"  - Request data keys: {list(request_data.keys())}")
                delegation_requests = getattr(
                    self.hook_handler, "delegation_requests", {}
                )
                _log(f"  - delegation_requests size: {len(delegation_requests)}")

            # Log important delegations for debugging
            if DEBUG or agent_type in ["research", "engineer", "qa", "documentation"]:
                _log(
                    f"Hook handler: Task delegation started - agent: '{agent_type}', session: '{session_id}'"
                )

        # Trigger memory pre-delegation hook
        try:
            mhm = getattr(self.hook_handler, "memory_hook_manager", None)
            if mhm and hasattr(mhm, "trigger_pre_delegation_hook"):
                mhm.trigger_pre_delegation_hook(agent_type, tool_input, session_id)
        except Exception:  # nosec B110
            # Memory hooks are optional
            pass

        # Emit a subagent_start event for better tracking
        subagent_start_data = {
            "agent_type": agent_type,
            "agent_id": f"{agent_type}_{session_id}",
            "session_id": session_id,
            "prompt": tool_input.get("prompt", ""),
            "description": tool_input.get("description", ""),
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "SubagentStart",  # For dashboard compatibility
        }
        self.hook_handler._emit_socketio_event(
            "", "subagent_start", subagent_start_data
        )

        # Log agent prompt if LogManager is available
        # Uses injected log_manager or lazy-loaded module-level instance
        log_manager = self.log_manager
        if log_manager is not None:
            try:
                # Prepare prompt content
                prompt_content = tool_input.get("prompt", "")
                if not prompt_content:
                    prompt_content = tool_input.get("description", "")

                if prompt_content:
                    # Prepare metadata
                    metadata = {
                        "agent_type": agent_type,
                        "agent_id": f"{agent_type}_{session_id}",
                        "session_id": session_id,
                        "delegation_context": {
                            "description": tool_input.get("description", ""),
                            "timestamp": datetime.now(timezone.utc).isoformat(),
                        },
                    }

                    # Log the agent prompt asynchronously
                    try:
                        loop = asyncio.get_running_loop()
                        _task = asyncio.create_task(
                            log_manager.log_prompt(
                                f"agent_{agent_type}", prompt_content, metadata
                            )
                        )  # Fire-and-forget logging (ephemeral hook process)
                    except RuntimeError:
                        # No running loop, create one
                        loop = asyncio.new_event_loop()
                        asyncio.set_event_loop(loop)
                        loop.run_until_complete(
                            log_manager.log_prompt(
                                f"agent_{agent_type}", prompt_content, metadata
                            )
                        )

                    if DEBUG:
                        _log(f"  - Agent prompt logged for {agent_type}")
            except Exception as e:
                if DEBUG:
                    _log(f"  - Could not log agent prompt: {e}")

    def _save_project_alias_if_present(self, prompt: str) -> None:
        """Detect @alias in prompt and save to state file for sticky context.

        WHY this feature:
        - Enables 'sticky' project context for subsequent prompts
        - User types '@myproject do something' once, then future prompts
          without @ automatically use the same project context
        - State file: ~/.claude-mpm/state/last_project.json

        Format: {"alias": "myproject", "timestamp": "..."}
        """
        if not prompt:
            return

        # Pattern: @alias at start of prompt (project context reference)
        # Matches @word but not @@ or email-like patterns
        match = re.match(r"^@([a-zA-Z][a-zA-Z0-9_-]*)\s", prompt)
        if not match:
            return

        alias = match.group(1)

        # Save to state file
        try:
            state_dir = Path.home() / ".claude-mpm" / "state"
            state_dir.mkdir(parents=True, exist_ok=True)

            state_file = state_dir / "last_project.json"
            state_data = {
                "alias": alias,
                "timestamp": datetime.now(timezone.utc).isoformat(),
            }

            with open(state_file, "w") as f:
                json.dump(state_data, f, indent=2)

            if DEBUG:
                _log(f"Saved project alias '{alias}' to {state_file}")

        except Exception as e:
            if DEBUG:
                _log(f"Failed to save project alias: {e}")
            # Non-fatal: sticky context is a convenience feature

    def _capture_pm_directive(self, prompt: str, project: Optional[str] = None) -> None:
        """Capture PM-level directive to persistent memory.

        Stores user orchestration commands for context enrichment:
        - Preferences ("always use PR model")
        - Workflows ("when deploying, run tests first")
        - Directives ("implement feature X")

        Args:
            prompt: User prompt to capture
            project: Project context (from @alias or cwd)
        """
        # Skip internal commands and very short prompts
        if prompt.startswith("/") or len(prompt) < 10:
            return

        try:
            from claude_mpm.memory import get_pm_memory

            pm_memory = get_pm_memory(enabled=True)
            pm_memory.capture_directive(prompt, project=project)

            if DEBUG:
                _log(f"Captured PM directive for project '{project}'")

        except ImportError:
            # kuzu-memory not installed - silently skip
            pass
        except Exception as e:
            if DEBUG:
                _log(f"Failed to capture PM directive: {e}")
            # Non-fatal: memory capture is optional

    def _get_git_branch(self, working_dir: Optional[str] = None) -> str:
        """Get git branch for the given directory with caching."""
        # Use current working directory if not specified
        if not working_dir:
            working_dir = Path.cwd()

        # Check cache first (cache for 300 seconds = 5 minutes)
        # WHY 5 minutes: Git branches rarely change during development sessions,
        # reducing subprocess overhead significantly without staleness issues
        current_time = datetime.now(timezone.utc).timestamp()
        cache_key = working_dir

        if (
            cache_key in self.hook_handler._git_branch_cache
            and cache_key in self.hook_handler._git_branch_cache_time
            and current_time - self.hook_handler._git_branch_cache_time[cache_key] < 300
        ):
            return self.hook_handler._git_branch_cache[cache_key]

        # Try to get git branch
        try:
            # Change to the working directory temporarily
            original_cwd = Path.cwd()
            os.chdir(working_dir)

            # Run git command to get current branch
            result = subprocess.run(  # nosec B603 B607
                ["git", "branch", "--show-current"],
                capture_output=True,
                text=True,
                timeout=TimeoutConfig.QUICK_TIMEOUT,
                check=False,  # Quick timeout to avoid hanging
            )

            # Restore original directory
            os.chdir(original_cwd)

            if result.returncode == 0 and result.stdout.strip():
                branch = result.stdout.strip()
                # Cache the result
                self.hook_handler._git_branch_cache[cache_key] = branch
                self.hook_handler._git_branch_cache_time[cache_key] = current_time
                return branch
            # Not a git repository or no branch
            self.hook_handler._git_branch_cache[cache_key] = "Unknown"
            self.hook_handler._git_branch_cache_time[cache_key] = current_time
            return "Unknown"

        except (
            subprocess.TimeoutExpired,
            subprocess.CalledProcessError,
            FileNotFoundError,
            OSError,
        ):
            # Git not available or command failed
            self.hook_handler._git_branch_cache[cache_key] = "Unknown"
            self.hook_handler._git_branch_cache_time[cache_key] = current_time
            return "Unknown"

    def _check_paused_session_tasks(self, working_dir: str) -> dict:
        """Check for paused sessions with pending tasks.

        Looks for ACTIVE-PAUSE.jsonl or LATEST-SESSION.txt and extracts
        task list information to include in session start data.

        Returns:
            Dict with has_pending_tasks and pending_task_count
        """
        result = {"has_pending_tasks": False, "pending_task_count": 0}

        try:
            sessions_dir = Path(working_dir) / ".claude-mpm" / "sessions"
            if not sessions_dir.exists():
                return result

            # Check for active pause first
            active_pause = sessions_dir / "ACTIVE-PAUSE.jsonl"
            if active_pause.exists():
                try:
                    with open(active_pause) as f:
                        lines = f.readlines()
                        if lines:
                            last_action = json.loads(lines[-1])
                            task_list = last_action.get("data", {}).get("task_list", {})
                            pending = len(task_list.get("pending_tasks", []))
                            in_progress = len(task_list.get("in_progress_tasks", []))
                            if pending + in_progress > 0:
                                result["has_pending_tasks"] = True
                                result["pending_task_count"] = pending + in_progress
                                return result
                except (json.JSONDecodeError, KeyError):
                    pass  # nosec B110 - continue to check regular sessions

            # Check for latest session
            latest_ptr = sessions_dir / "LATEST-SESSION.txt"
            if latest_ptr.exists():
                try:
                    session_name = latest_ptr.read_text().strip()
                    session_file = sessions_dir / f"{session_name}.json"
                    if session_file.exists():
                        with open(session_file) as f:
                            session_data = json.load(f)
                            task_list = session_data.get("task_list", {})
                            pending = len(task_list.get("pending_tasks", []))
                            in_progress = len(task_list.get("in_progress_tasks", []))
                            if pending + in_progress > 0:
                                result["has_pending_tasks"] = True
                                result["pending_task_count"] = pending + in_progress
                except (json.JSONDecodeError, KeyError, FileNotFoundError):
                    pass  # nosec B110 - return default result

        except Exception:
            pass  # nosec B110 - lightweight check, don't fail session start

        return result

    def handle_post_tool_fast(self, event):
        """Handle post-tool use with comprehensive data capture.

        WHY comprehensive capture:
        - Captures execution results and success/failure status
        - Provides duration and performance metrics
        - Enables pattern analysis of tool usage and success rates
        """
        tool_name = event.get("tool_name", "")
        exit_code = event.get("exit_code", 0)
        session_id = event.get("session_id", "")

        # Extract result data
        result_data = extract_tool_results(event)

        # Calculate duration if timestamps are available
        duration = calculate_duration(event)

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        # Retrieve tool_call_id using CorrelationManager for cross-process correlation
        tool_call_id = CorrelationManager.retrieve(session_id) if session_id else None
        if DEBUG and tool_call_id:
            _log(
                f"  - Retrieved tool_call_id: {tool_call_id[:8]}... for session {session_id[:8]}..."
            )

        post_tool_data = {
            "tool_name": tool_name,
            "exit_code": exit_code,
            "success": exit_code == 0,
            "status": (
                "success"
                if exit_code == 0
                else "blocked"
                if exit_code == 2
                else "error"
            ),
            "duration_ms": duration,
            "result_summary": result_data,
            "session_id": session_id,
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "has_output": bool(result_data.get("output")),
            "has_error": bool(result_data.get("error")),
            "output_size": (
                len(str(result_data.get("output", "")))
                if result_data.get("output")
                else 0
            ),
        }

        # Include full output for file operations (Read, Edit, Write)
        # so frontend can display file content
        if tool_name in ("Read", "Edit", "Write", "Grep", "Glob") and "output" in event:
            post_tool_data["output"] = event["output"]

        # Add correlation_id if available for correlation with pre_tool
        if tool_call_id:
            post_tool_data["correlation_id"] = tool_call_id

        # Handle Task delegation completion for memory hooks and response tracking
        if tool_name == "Task":
            session_id = event.get("session_id", "")
            agent_type = self.hook_handler._get_delegation_agent_type(session_id)

            # Trigger memory post-delegation hook
            try:
                mhm = getattr(self.hook_handler, "memory_hook_manager", None)
                if mhm and hasattr(mhm, "trigger_post_delegation_hook"):
                    mhm.trigger_post_delegation_hook(agent_type, event, session_id)
            except Exception:  # nosec B110
                # Memory hooks are optional
                pass

            # Track agent response if response tracking is enabled
            try:
                rtm = getattr(self.hook_handler, "response_tracking_manager", None)
                if rtm and hasattr(rtm, "track_agent_response"):
                    delegation_requests = getattr(
                        self.hook_handler, "delegation_requests", {}
                    )
                    rtm.track_agent_response(
                        session_id, agent_type, event, delegation_requests
                    )
            except Exception:  # nosec B110
                # Response tracking is optional
                pass

        self.hook_handler._emit_socketio_event("", "post_tool", post_tool_data)

    def handle_notification_fast(self, event):
        """Handle notification events from Claude.

        WHY enhanced notification capture:
        - Provides visibility into Claude's status and communication flow
        - Captures notification type, content, and context for monitoring
        - Enables pattern analysis of Claude's notification behavior
        - Useful for debugging communication issues and user experience
        """
        notification_type = event.get("notification_type", "unknown")
        message = event.get("message", "")

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        notification_data = {
            "notification_type": notification_type,
            "message": message,
            "message_preview": message[:200] if len(message) > 200 else message,
            "message_length": len(message),
            "session_id": event.get("session_id", ""),
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "is_user_input_request": "input" in message.lower()
            or "waiting" in message.lower(),
            "is_error_notification": "error" in message.lower()
            or "failed" in message.lower(),
            "is_status_update": any(
                word in message.lower()
                for word in ["processing", "analyzing", "working", "thinking"]
            ),
        }

        # Emit normalized event
        self.hook_handler._emit_socketio_event("", "notification", notification_data)

    def handle_stop_fast(self, event):
        """Handle stop events when Claude processing stops.

        WHY comprehensive stop capture:
        - Provides visibility into Claude's session lifecycle
        - Captures stop reason and context for analysis
        - Enables tracking of session completion patterns
        - Useful for understanding when and why Claude stops responding
        """
        session_id = event.get("session_id", "")

        # Extract metadata for this stop event
        metadata = self._extract_stop_metadata(event)

        # Debug logging
        if DEBUG:
            self._log_stop_event_debug(event, session_id, metadata)

        # Auto-pause integration (independent of response tracking)
        # WHY HERE: Auto-pause must work even when response_tracking is disabled
        # Extract usage data directly from event and trigger auto-pause if thresholds crossed
        if "usage" in event:
            auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
            if auto_pause:
                try:
                    usage_data = event["usage"]
                    metadata["usage"] = {
                        "input_tokens": usage_data.get("input_tokens", 0),
                        "output_tokens": usage_data.get("output_tokens", 0),
                        "cache_creation_input_tokens": usage_data.get(
                            "cache_creation_input_tokens", 0
                        ),
                        "cache_read_input_tokens": usage_data.get(
                            "cache_read_input_tokens", 0
                        ),
                    }

                    threshold_crossed = auto_pause.on_usage_update(metadata["usage"])
                    if threshold_crossed:
                        warning = auto_pause.emit_threshold_warning(threshold_crossed)
                        # CRITICAL: Never write to stderr unconditionally - causes hook errors
                        # Use _log() instead which only writes to file if DEBUG=true
                        _log(f"⚠️  Auto-pause threshold crossed: {warning}")

                        if DEBUG:
                            _log(
                                f"  - Auto-pause threshold crossed: {threshold_crossed}"
                            )
                except Exception as e:
                    if DEBUG:
                        _log(f"Auto-pause error in handle_stop_fast: {e}")

                # Finalize pause session if active
                try:
                    if auto_pause.is_pause_active():
                        session_file = auto_pause.on_session_end()
                        if session_file:
                            if DEBUG:
                                _log(
                                    f"✅ Auto-pause session finalized: {session_file.name}"
                                )
                except Exception as e:
                    if DEBUG:
                        _log(f"❌ Failed to finalize auto-pause session: {e}")

        # Track response if enabled
        try:
            rtm = getattr(self.hook_handler, "response_tracking_manager", None)
            if rtm and hasattr(rtm, "track_stop_response"):
                pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
                rtm.track_stop_response(event, session_id, metadata, pending_prompts)
        except Exception:  # nosec B110
            # Response tracking is optional
            pass

        # Check for unread cross-project messages
        # If unread messages exist AND this isn't a re-triggered stop (stop_hook_active),
        # block the stop so Claude sees the unread messages and can act on them.
        try:
            from claude_mpm.core.unified_paths import UnifiedPathManager
            from claude_mpm.services.communication.message_service import MessageService

            # Don't block if this stop was already triggered by a previous block
            # (stop_hook_active prevents infinite loop)
            stop_hook_active = event.get("stop_hook_active", False)
            if not stop_hook_active:
                project_root = UnifiedPathManager().project_root
                service = MessageService(project_root)
                unread = service.list_messages(status="unread")
                if unread:
                    _log(
                        f"📬 {len(unread)} unread cross-project message(s) at session end - blocking stop"
                    )

                    # Build summary
                    from collections import Counter

                    sources = Counter(Path(m.from_project).name for m in unread)
                    source_summary = ", ".join(
                        f"{count} from {name}" for name, count in sources.most_common()
                    )

                    high_priority = [
                        m for m in unread if m.priority in ("high", "urgent")
                    ]
                    priority_note = (
                        f" ({len(high_priority)} high priority)"
                        if high_priority
                        else ""
                    )

                    reason = (
                        f"📬 {len(unread)} unread cross-project message(s){priority_note}: "
                        f"{source_summary}. "
                        f"Read and act on them with: `claude-mpm message list --status unread`"
                    )

                    return {"decision": "block", "reason": reason}
        except Exception as e:
            if DEBUG:
                _log(f"Message check on stop error: {e}")

        # Emit stop event to Socket.IO
        self._emit_stop_event(event, session_id, metadata)
        return None

    def _extract_stop_metadata(self, event: dict) -> dict:
        """Extract metadata from stop event."""
        working_dir = event.get("cwd", "")
        return {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "working_directory": working_dir,
            "git_branch": (
                self._get_git_branch(working_dir) if working_dir else "Unknown"
            ),
            "event_type": "stop",
            "reason": event.get("reason", "unknown"),
            "stop_type": event.get("stop_type", "normal"),
        }

    def _log_stop_event_debug(
        self, event: dict, session_id: str, metadata: dict
    ) -> None:
        """Log debug information for stop events."""
        try:
            rtm = getattr(self.hook_handler, "response_tracking_manager", None)
            tracking_enabled = (
                getattr(rtm, "response_tracking_enabled", False) if rtm else False
            )
            tracker_exists = (
                getattr(rtm, "response_tracker", None) is not None if rtm else False
            )

            _log(f"  - response_tracking_enabled: {tracking_enabled}")
            _log(f"  - response_tracker exists: {tracker_exists}")
        except Exception:  # nosec B110
            # If debug logging fails, just skip it
            pass

        _log(f"  - session_id: {session_id[:8] if session_id else 'None'}...")
        _log(f"  - reason: {metadata['reason']}")
        _log(f"  - stop_type: {metadata['stop_type']}")

    def _emit_stop_event(self, event: dict, session_id: str, metadata: dict) -> None:
        """Emit stop event data to Socket.IO."""
        stop_data = {
            "reason": metadata["reason"],
            "stop_type": metadata["stop_type"],
            "session_id": session_id,
            "working_directory": metadata["working_directory"],
            "git_branch": metadata["git_branch"],
            "timestamp": metadata["timestamp"],
            "is_user_initiated": metadata["reason"]
            in ["user_stop", "user_cancel", "interrupt"],
            "is_error_stop": metadata["reason"] in ["error", "timeout", "failed"],
            "is_completion_stop": metadata["reason"]
            in ["completed", "finished", "done"],
            "has_output": bool(event.get("final_output")),
            "usage": metadata.get("usage"),  # Add token usage data
        }

        # Emit normalized event
        self.hook_handler._emit_socketio_event("", "stop", stop_data)

        # Emit dedicated token usage event if usage data is available
        if metadata.get("usage"):
            usage_data = metadata["usage"]
            token_usage_data = {
                "session_id": session_id,
                "input_tokens": usage_data.get("input_tokens", 0),
                "output_tokens": usage_data.get("output_tokens", 0),
                "cache_creation_tokens": usage_data.get(
                    "cache_creation_input_tokens", 0
                ),
                "cache_read_tokens": usage_data.get("cache_read_input_tokens", 0),
                "total_tokens": (
                    usage_data.get("input_tokens", 0)
                    + usage_data.get("output_tokens", 0)
                    + usage_data.get("cache_creation_input_tokens", 0)
                    + usage_data.get("cache_read_input_tokens", 0)
                ),
                "timestamp": metadata["timestamp"],
            }
            self.hook_handler._emit_socketio_event(
                "", "token_usage_updated", token_usage_data
            )

    def handle_subagent_stop_fast(self, event):
        """Handle subagent stop events by delegating to the specialized processor."""
        # Delegate to the specialized subagent processor
        if hasattr(self.hook_handler, "subagent_processor"):
            self.hook_handler.subagent_processor.process_subagent_stop(event)
        else:
            # Fallback to handle_subagent_stop if processor not available
            self.hook_handler.handle_subagent_stop(event)

    def _handle_subagent_response_tracking(
        self,
        session_id: str,
        agent_type: str,
        reason: str,
        output: str,
        structured_response: dict,
        working_dir: str,
        git_branch: str,
    ):
        """Handle response tracking for subagent stop events with fuzzy matching."""
        try:
            rtm = getattr(self.hook_handler, "response_tracking_manager", None)
            if not (
                rtm
                and getattr(rtm, "response_tracking_enabled", False)
                and getattr(rtm, "response_tracker", None)
            ):
                return
        except Exception:
            # Response tracking is optional
            return

        try:
            # Get the original request data (with fuzzy matching fallback)
            delegation_requests = getattr(self.hook_handler, "delegation_requests", {})
            request_info = delegation_requests.get(session_id)  # nosec B113

            # If exact match fails, try partial matching
            if not request_info and session_id:
                if DEBUG:
                    _log(f"  - Trying fuzzy match for session {session_id[:16]}...")
                # Try to find a session that matches the first 8-16 characters
                for stored_sid in list(delegation_requests.keys()):
                    if (
                        stored_sid.startswith(session_id[:8])
                        or session_id.startswith(stored_sid[:8])
                        or (
                            len(session_id) >= 16
                            and len(stored_sid) >= 16
                            and stored_sid[:16] == session_id[:16]
                        )
                    ):
                        if DEBUG:
                            _log(f"  - ✅ Fuzzy match found: {stored_sid[:16]}...")
                        request_info = delegation_requests.get(stored_sid)  # nosec B113
                        # Update the key to use the current session_id for consistency
                        if request_info:
                            delegation_requests[session_id] = request_info
                            # Optionally remove the old key to avoid duplicates
                            if stored_sid != session_id:
                                del delegation_requests[stored_sid]
                        break

            if request_info:
                # Use the output as the response
                response_text = (
                    str(output)
                    if output
                    else f"Agent {agent_type} completed with reason: {reason}"
                )

                # Get the original request
                original_request = request_info.get("request", {})
                prompt = original_request.get("prompt", "")
                description = original_request.get("description", "")

                # Combine prompt and description
                full_request = prompt
                if description and description != prompt:
                    if full_request:
                        full_request += f"\n\nDescription: {description}"
                    else:
                        full_request = description

                if not full_request:
                    full_request = f"Task delegation to {agent_type} agent"

                # Prepare metadata
                metadata = {
                    "exit_code": 0,  # SubagentStop doesn't have exit_code
                    "success": reason in ["completed", "finished", "done"],
                    "has_error": reason in ["error", "timeout", "failed", "blocked"],
                    "working_directory": working_dir,
                    "git_branch": git_branch,
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                    "event_type": "subagent_stop",
                    "reason": reason,
                    "original_request_timestamp": request_info.get("timestamp"),
                }

                # Add structured response if available
                if structured_response:
                    metadata["structured_response"] = structured_response
                    metadata["task_completed"] = structured_response.get(
                        "task_completed", False
                    )

                # Track the response
                rtm = getattr(self.hook_handler, "response_tracking_manager", None)
                response_tracker = (
                    getattr(rtm, "response_tracker", None) if rtm else None
                )
                if response_tracker and hasattr(response_tracker, "track_response"):
                    file_path = response_tracker.track_response(
                        agent_name=agent_type,
                        request=full_request,
                        response=response_text,
                        session_id=session_id,
                        metadata=metadata,
                    )

                    if file_path and DEBUG:
                        _log(
                            f"✅ Tracked {agent_type} agent response on SubagentStop: {file_path.name}"
                        )

                # Clean up the request data
                delegation_requests = getattr(
                    self.hook_handler, "delegation_requests", {}
                )
                if session_id in delegation_requests:
                    del delegation_requests[session_id]

            elif DEBUG:
                _log(
                    f"No request data for SubagentStop session {session_id[:8]}..., agent: {agent_type}"
                )

        except Exception as e:
            if DEBUG:
                _log(f"❌ Failed to track response on SubagentStop: {e}")

    def handle_assistant_response(self, event):
        """Handle assistant response events for comprehensive response tracking.

        WHY emit assistant response events:
        - Provides visibility into Claude's responses to user prompts
        - Captures response content and metadata for analysis
        - Enables tracking of conversation flow and response patterns
        - Essential for comprehensive monitoring of Claude interactions
        - Scans for delegation anti-patterns and creates autotodos
        """
        # Track the response for logging
        try:
            rtm = getattr(self.hook_handler, "response_tracking_manager", None)
            if rtm and hasattr(rtm, "track_assistant_response"):
                pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
                rtm.track_assistant_response(event, pending_prompts)
        except Exception:  # nosec B110
            # Response tracking is optional
            pass

        # Scan response for delegation anti-patterns and create autotodos
        try:
            self._scan_for_delegation_patterns(event)
        except Exception as e:  # nosec B110
            if DEBUG:
                _log(f"Delegation scanning error: {e}")

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        # Extract response data
        response_text = event.get("response", "")
        session_id = event.get("session_id", "")

        # Prepare assistant response data for Socket.IO emission
        assistant_response_data = {
            "response_text": response_text,
            "response_preview": (
                response_text[:500] if len(response_text) > 500 else response_text
            ),
            "response_length": len(response_text),
            "session_id": session_id,
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "contains_code": "```" in response_text,
            "contains_json": "```json" in response_text,
            "hook_event_name": "AssistantResponse",  # Explicitly set for dashboard
            "has_structured_response": bool(
                re.search(r"```json\s*\{.*?\}\s*```", response_text, re.DOTALL)
            ),
        }

        # Check if this is a response to a tracked prompt
        try:
            pending_prompts = getattr(self.hook_handler, "pending_prompts", {})
            if session_id in pending_prompts:
                prompt_data = pending_prompts[session_id]
                assistant_response_data["original_prompt"] = prompt_data.get(
                    "prompt", ""
                )[:200]
                assistant_response_data["prompt_timestamp"] = prompt_data.get(
                    "timestamp", ""
                )
                assistant_response_data["is_tracked_response"] = True
            else:
                assistant_response_data["is_tracked_response"] = False
        except Exception:
            # If prompt lookup fails, just mark as not tracked
            assistant_response_data["is_tracked_response"] = False

        # Debug logging
        if DEBUG:
            _log(
                f"Hook handler: Processing AssistantResponse - session: '{session_id}', response_length: {len(response_text)}"
            )

        # Record assistant response for auto-pause if active
        auto_pause = getattr(self.hook_handler, "auto_pause_handler", None)
        if auto_pause and auto_pause.is_pause_active():
            try:
                # Summarize response to first 200 chars
                summary = (
                    response_text[:200] + "..."
                    if len(response_text) > 200
                    else response_text
                )
                auto_pause.on_assistant_response(summary)
            except Exception as e:
                if DEBUG:
                    _log(f"Auto-pause response recording error: {e}")

        # Emit normalized event
        self.hook_handler._emit_socketio_event(
            "", "assistant_response", assistant_response_data
        )

    def handle_session_start_fast(self, event):
        """Handle session start events for tracking conversation sessions.

        WHY track session starts:
        - Provides visibility into new conversation sessions
        - Enables tracking of session lifecycle and duration
        - Useful for monitoring concurrent sessions and resource usage

        NOTE: This handler is intentionally lightweight - only event monitoring.
        All initialization/deployment logic runs in MPM CLI startup, not here.
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        session_start_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "git_branch": git_branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "SessionStart",
            "has_pending_tasks": False,
            "pending_task_count": 0,
        }

        # Check for paused sessions with pending tasks
        if working_dir:
            session_start_data.update(self._check_paused_session_tasks(working_dir))

        # Debug logging
        _log(
            f"Hook handler: Processing SessionStart - session: '{session_id}', pending_tasks: {session_start_data.get('pending_task_count', 0)}"
        )

        # Emit normalized event
        self.hook_handler._emit_socketio_event("", "session_start", session_start_data)

    def handle_subagent_start_fast(self, event):
        """Handle SubagentStart events with proper agent type extraction.

        WHY separate from SessionStart:
        - SubagentStart contains agent-specific information
        - Frontend needs agent_type to create distinct agent nodes
        - Multiple engineers should show as separate nodes in hierarchy
        - Research agents must appear in the agent hierarchy

        Unlike SessionStart, SubagentStart events contain agent-specific
        information that must be preserved and emitted to the dashboard.
        """
        session_id = event.get("session_id", "")

        # Extract agent type from event - Claude provides this in SubagentStart
        # Try multiple possible field names for compatibility
        agent_type = event.get("agent_type") or event.get("subagent_type") or "unknown"

        # Generate unique agent ID combining type and session
        agent_id = event.get("agent_id", f"{agent_type}_{session_id[:8]}")

        # Get working directory and git branch
        working_dir = event.get("cwd", "")
        git_branch = self._get_git_branch(working_dir) if working_dir else "Unknown"

        # Build subagent start data with all required fields
        subagent_start_data = {
            "session_id": session_id,
            "agent_type": agent_type,
            "agent_id": agent_id,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "SubagentStart",  # Preserve correct hook name
            "working_directory": working_dir,
            "git_branch": git_branch,
        }

        # Debug logging
        if DEBUG:
            _log(
                f"Hook handler: SubagentStart - agent_type='{agent_type}', agent_id='{agent_id}', session_id='{session_id[:16]}...'"
            )

        # Emit to /hook namespace as subagent_start (NOT session_start!)
        self.hook_handler._emit_socketio_event(
            "", "subagent_start", subagent_start_data
        )

    def handle_worktree_create_fast(self, event):
        """Handle WorktreeCreate hook event (Claude Code v2.1.47+).

        Fires when `claude --worktree` creates a new git worktree, indicating
        a new parallel work session is starting. This is useful for the
        dashboard to show concurrent work happening in isolated branches.

        Key fields from Claude Code:
        - worktree_name: Human-readable name for the worktree
        - worktree_path: Absolute filesystem path to the new worktree
        - branch: Git branch name checked out in the worktree
        - session_id: The session that triggered the worktree creation
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")

        # Extract worktree-specific fields
        worktree_name = event.get("worktree_name", "")
        worktree_path = event.get("worktree_path", event.get("path", ""))
        branch = event.get("branch", event.get("worktree_branch", ""))

        worktree_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "worktree_name": worktree_name,
            "worktree_path": worktree_path,
            "branch": branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "WorktreeCreate",
        }

        _log(
            f"Hook handler: WorktreeCreate - worktree='{worktree_name}', "
            f"path='{worktree_path}', branch='{branch}', session='{session_id[:16]}...'"
        )

        self.hook_handler._emit_socketio_event("", "worktree_create", worktree_data)

    def handle_worktree_remove_fast(self, event):
        """Handle WorktreeRemove hook event (Claude Code v2.1.47+).

        Fires when a git worktree created by `claude --worktree` is removed,
        indicating a parallel work session has ended.

        Key fields from Claude Code:
        - worktree_name: Human-readable name of the removed worktree
        - worktree_path: Absolute filesystem path of the removed worktree
        - branch: Git branch that was checked out in the removed worktree
        - session_id: The session that triggered the worktree removal
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")

        # Extract worktree-specific fields
        worktree_name = event.get("worktree_name", "")
        worktree_path = event.get("worktree_path", event.get("path", ""))
        branch = event.get("branch", event.get("worktree_branch", ""))

        worktree_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "worktree_name": worktree_name,
            "worktree_path": worktree_path,
            "branch": branch,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "WorktreeRemove",
        }

        _log(
            f"Hook handler: WorktreeRemove - worktree='{worktree_name}', "
            f"path='{worktree_path}', branch='{branch}', session='{session_id[:16]}...'"
        )

        self.hook_handler._emit_socketio_event("", "worktree_remove", worktree_data)

    def handle_config_change_fast(self, event):
        """Handle ConfigChange hook event (Claude Code v2.1.47+).

        Fires when `.claude/` settings or skills files change mid-session.
        Subtypes include: user_settings, project_settings, local_settings,
        policy_settings, skills.

        This allows mpm to react when users modify their Claude Code
        configuration without restarting (e.g., adding a new skill or
        changing a project-level setting).
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")

        # Extract config change details
        # The subtype identifies which settings file changed
        subtype = event.get("subtype", event.get("config_type", "unknown"))
        changed_file = event.get("file", event.get("config_file", ""))
        change_type = event.get("change_type", "modified")  # created/modified/deleted

        config_change_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "subtype": subtype,
            "changed_file": changed_file,
            "change_type": change_type,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "ConfigChange",
        }

        _log(
            f"Hook handler: ConfigChange - subtype='{subtype}', "
            f"file='{changed_file}', change_type='{change_type}'"
        )

        self.hook_handler._emit_socketio_event("", "config_change", config_change_data)

    def handle_teammate_idle_fast(self, event):
        """Handle TeammateIdle hook event (Claude Code v2.1.47+ Agent Teams).

        Fires when an agent team teammate goes idle (experimental agent teams
        feature). This allows mpm to track when members of a coordinated
        agent team pause, enabling visibility into team-level work patterns
        in the dashboard.

        Note: This is an experimental feature and the event schema may
        evolve as Claude Code's agent teams feature matures.
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")

        # Extract teammate information
        teammate_id = event.get("teammate_id", event.get("agent_id", ""))
        teammate_type = event.get("teammate_type", event.get("agent_type", "unknown"))
        idle_reason = event.get("reason", event.get("idle_reason", "unknown"))

        teammate_idle_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "teammate_id": teammate_id,
            "teammate_type": teammate_type,
            "idle_reason": idle_reason,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "TeammateIdle",
        }

        _log(
            f"Hook handler: TeammateIdle - teammate_id='{teammate_id}', "
            f"type='{teammate_type}', reason='{idle_reason}'"
        )

        self.hook_handler._emit_socketio_event("", "teammate_idle", teammate_idle_data)

    def handle_task_completed_fast(self, event):
        """Handle TaskCompleted hook event (Claude Code v2.1.47+ Agent Teams).

        Fires when a task is marked complete in agent teams. Enables tracking
        task completion progress across coordinated agent team workflows.
        Provides visibility into which agents are completing work and at what
        rate, even if the user is not actively using mpm's PM features.

        Note: This is an experimental feature and the event schema may
        evolve as Claude Code's agent teams feature matures.
        """
        session_id = event.get("session_id", "")
        working_dir = event.get("cwd", "")

        # Extract task completion details
        task_id = event.get("task_id", "")
        task_title = event.get("task_title", event.get("title", ""))
        completed_by = event.get("completed_by", event.get("agent_id", ""))
        completion_status = event.get("status", "completed")

        task_completed_data = {
            "session_id": session_id,
            "working_directory": working_dir,
            "task_id": task_id,
            "task_title": task_title,
            "completed_by": completed_by,
            "completion_status": completion_status,
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "hook_event_name": "TaskCompleted",
        }

        _log(
            f"Hook handler: TaskCompleted - task_id='{task_id}', "
            f"title='{task_title}', completed_by='{completed_by}'"
        )

        self.hook_handler._emit_socketio_event(
            "", "task_completed", task_completed_data
        )

    def _scan_for_delegation_patterns(self, event):
        """Scan assistant response for delegation anti-patterns.

        WHY this is needed:
        - Detect when PM asks user to do something manually instead of delegating
        - Flag PM behavior violations for immediate correction
        - Enforce delegation principle in PM workflow
        - Help PM recognize delegation opportunities

        This method scans the assistant's response text for patterns like:
        - "Make sure .env.local is in your .gitignore"
        - "You'll need to run npm install"
        - "Please run the tests manually"

        When patterns are detected, PM violations are logged as errors/warnings
        that should be corrected immediately, NOT as todos to delegate.

        DESIGN DECISION: pm.violation vs autotodo.delegation
        - Delegation patterns = PM doing something WRONG → pm.violation (error)
        - Script failures = Something BROKEN → autotodo.error (todo)
        """
        # Only scan if delegation detector and event log are available
        # Uses injected services or lazy-loaded module-level instances
        detector = self.delegation_detector
        event_log_service = self.event_log

        if detector is None or event_log_service is None:
            if DEBUG:
                _log("Delegation detector or event log not available")
            return

        response_text = event.get("response", "")
        if not response_text:
            return

        # Detect delegation patterns
        detections = detector.detect_user_delegation(response_text)

        if not detections:
            return  # No patterns detected

        # Create PM violation events (NOT autotodos)
        for detection in detections:
            # Create event log entry as pm.violation
            event_log_service.append_event(
                event_type="pm.violation",
                payload={
                    "violation_type": "delegation_anti_pattern",
                    "pattern_type": detection["pattern_type"],
                    "original_text": detection["original_text"],
                    "suggested_action": detection["suggested_todo"],
                    "action": detection["action"],
                    "session_id": event.get("session_id", ""),
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                    "severity": "warning",  # Not critical, but should be fixed
                    "message": f"PM asked user to do something manually: {detection['original_text'][:80]}...",
                },
                status="pending",
            )

            if DEBUG:
                _log(f"⚠️  PM violation detected: {detection['original_text'][:60]}...")
