"""Agent Orchestrator Plugin for spawning and managing parallel kollab sub-agents.

This plugin enables the LLM to spawn sub-agents via XML commands in its responses.
Sub-agents run in tmux sessions and are monitored for completion via MD5 hashing.
"""

import argparse
import asyncio
import logging
import re
import sys
import tempfile
import json
from pathlib import Path
from typing import Dict, Any, Optional

from core.plugins.base import BasePlugin
from core.events.models import EventType, Hook, HookPriority
from core.io.visual_effects import ColorPalette
from core.io.message_renderer import DisplayFilterRegistry, MessageType

from .xml_parser import XMLCommandParser
from .orchestrator import AgentOrchestrator
from .activity_monitor import ActivityMonitor
from .message_injector import MessageInjector
from .models import AgentTask

logger = logging.getLogger(__name__)

# Pre-compiled regex patterns for performance (used in display filter)
_ORCH_QUICK_CHECK = re.compile(r'<(?:agent|status|capture|stop|message|clone|team|broadcast|sys_msg)', re.IGNORECASE)
_STRIP_AGENT = re.compile(r"<agent>.*?</agent>\s*", re.DOTALL)
_STRIP_STATUS = re.compile(r"<status>\s*</status>\s*")
_STRIP_CAPTURE = re.compile(r"<capture>.*?</capture>\s*", re.DOTALL)
_STRIP_STOP = re.compile(r"<stop>.*?</stop>\s*", re.DOTALL)
_STRIP_MESSAGE = re.compile(r'<message\s+to=["\'][^"\']+["\']>.*?</message>\s*', re.DOTALL)
_STRIP_CLONE = re.compile(r"<clone>.*?</clone>\s*", re.DOTALL)
_STRIP_TEAM = re.compile(r"<team\s+[^>]*>.*?</team>\s*", re.DOTALL)
_STRIP_BROADCAST = re.compile(r"<broadcast\s+[^>]*>.*?</broadcast>\s*", re.DOTALL)
_STRIP_SYS_MSG = re.compile(r"<sys_msg>.*?</sys_msg>\s*", re.DOTALL)

# Agent orchestration instructions for the LLM
AGENT_INSTRUCTIONS = """
## Agent Orchestration

You can spawn parallel sub-agents to work on tasks concurrently. Each agent runs in a separate tmux session.

IMPORTANT: When using agent commands, output ONLY the XML tag. Do NOT add any explanation text before or after the command. The system will display the command execution result automatically.

### Spawn Agents

```xml
<agent>
  <agent-name>
    <task>
    objective: What to accomplish

    context:
    - Relevant constraints
    - Background info

    todo:
    [ ] Step 1
    [ ] Step 2

    success: How to verify completion
    </task>
    <files>
      <file>path/to/file.py</file>
    </files>
  </agent-name>
</agent>
```

### Other Commands

```xml
<message to="agent-name">Send instruction to running agent</message>
<capture>agent-name 200</capture>
<stop>agent-name</stop>
<status></status>
```

### Command Behavior

These commands work like tool calls:
1. You output the XML command (no additional text)
2. System executes and shows: `⏺ spawn_agent(name)` or `⏺ status()`
3. Result is injected back to you
4. You can then respond to the result if needed

Do NOT say things like "I'll spawn an agent" or "I've spawned..." - just output the XML.

Files in `<files>` are auto-attached to agent context.
Agents complete when idle for 6 seconds (MD5 hash unchanged).
""".strip()

# Keywords that trigger instruction injection
TRIGGER_KEYWORDS = [
    "spawn agent",
    "spawn agents",
    "spawn an agent",
    "spawn one agent",
    "spawn multiple",
    "parallel agent",
    "parallel agents",
    "sub-agent",
    "sub-agents",
    "subagent",
    "subagents",
    "<agent>",
    "tmux agent",
    "agent orchestrat",
    "launch agent",
    "create agent",
]


class AgentOrchestratorPlugin(BasePlugin):
    """Plugin for spawning and managing parallel kollab sub-agents."""

    def __init__(
        self,
        name: str = "agent_orchestrator",
        event_bus=None,
        renderer=None,
        config=None,
    ):
        """Initialize the agent orchestrator plugin.

        Args:
            name: Plugin name.
            event_bus: Event bus for hook registration.
            renderer: Terminal renderer.
            config: Configuration manager.
        """
        self.name = name
        self.version = "1.0.0"
        self.description = "Spawn and manage parallel kollab sub-agents"
        self.enabled = True

        self.event_bus = event_bus
        self.renderer = renderer
        self.config = config
        self.command_registry = None
        self.conversation_manager = None

        # Components (initialized in initialize())
        self.xml_parser = XMLCommandParser()
        self.orchestrator: Optional[AgentOrchestrator] = None
        self.activity_monitor: Optional[ActivityMonitor] = None
        self.message_injector: Optional[MessageInjector] = None

        self._monitor_task: Optional[asyncio.Task] = None
        self._args: Optional[argparse.Namespace] = None

        # Tracking for keyword trigger mode
        self._last_injection_time: float = 0.0
        self._message_count_since_injection: int = 0

        self.logger = logger

    # -------------------------------------------------------------------------
    # CLI Args (static, called before app init)
    # -------------------------------------------------------------------------

    @staticmethod
    def register_cli_args(parser: argparse.ArgumentParser) -> None:
        """Register CLI arguments for agent management."""
        group = parser.add_argument_group("Agent Orchestrator")
        group.add_argument(
            "--session",
            type=str,
            metavar="NAME",
            help="Agent session name to interact with",
        )
        group.add_argument(
            "--capture",
            type=int,
            metavar="LINES",
            default=None,
            help="Capture N lines from session (requires --session)",
        )
        group.add_argument(
            "--list-agents",
            action="store_true",
            help="List all active agents and exit",
        )

    @staticmethod
    def handle_early_args(args: argparse.Namespace) -> tuple[bool, str | None]:
        """Handle args that exit before app starts.

        Returns:
            Tuple of (should_exit, output_message).
            If should_exit is True, output_message contains the text to display.
        """
        project_name = Path.cwd().name

        if getattr(args, "list_agents", False):
            orchestrator = AgentOrchestrator(project_name=project_name)
            agents = orchestrator.list_agents()
            if agents:
                lines = ["[agents]"]
                for agent in agents:
                    lines.append(f"  {agent.name:<20} {agent.status:<10} {agent.duration}")
                return (True, "\n".join(lines))
            else:
                return (True, "No active agents")

        session = getattr(args, "session", None)
        capture = getattr(args, "capture", None)

        if session and capture:
            orchestrator = AgentOrchestrator(project_name=project_name)
            output = orchestrator.capture_output(session, capture)
            return (True, output)

        return (False, None)  # continue normal startup

    # -------------------------------------------------------------------------
    # Plugin Lifecycle
    # -------------------------------------------------------------------------

    async def initialize(self, args: argparse.Namespace = None, **kwargs) -> None:
        """Initialize plugin components.

        Args:
            args: Parsed CLI arguments.
            **kwargs: Additional initialization parameters including:
                - event_bus: Event bus for hook registration
                - config: Configuration manager
                - command_registry: Command registry for slash commands
                - conversation_manager: Conversation manager instance
        """
        self._args = args

        # Get dependencies from kwargs
        if "event_bus" in kwargs:
            self.event_bus = kwargs["event_bus"]
        if "config" in kwargs:
            self.config = kwargs["config"]
        if "command_registry" in kwargs:
            self.command_registry = kwargs["command_registry"]
        if "conversation_manager" in kwargs:
            self.conversation_manager = kwargs["conversation_manager"]

        # Initialize orchestrator
        self.orchestrator = AgentOrchestrator(project_name=Path.cwd().name)

        # Initialize message injector if we have conversation manager
        if self.conversation_manager and self.event_bus:
            self.message_injector = MessageInjector(
                event_bus=self.event_bus,
                conversation_manager=self.conversation_manager,
            )

        # Get config values
        poll_interval = 2
        idle_threshold = 3
        capture_lines = 500

        if self.config:
            poll_interval = self.config.get("plugins.agent_orchestrator.poll_interval", 2)
            idle_threshold = self.config.get("plugins.agent_orchestrator.idle_threshold", 3)
            capture_lines = self.config.get("plugins.agent_orchestrator.capture_lines", 500)

        # Initialize activity monitor
        self.activity_monitor = ActivityMonitor(
            orchestrator=self.orchestrator,
            on_agent_complete=self._on_agent_complete,
            poll_interval=poll_interval,
            idle_threshold=idle_threshold,
            capture_lines=capture_lines,
        )

        # Register display filter to strip orchestrator XML from displayed messages
        # Applies to both ASSISTANT (XML commands) and USER (<sys_msg> tags)
        DisplayFilterRegistry.register(
            name="agent_orchestrator",
            filter_fn=self._strip_orchestrator_xml,
            message_types=[MessageType.ASSISTANT, MessageType.USER],
            priority=100,
        )

        # Register slash commands if command registry is available
        if self.command_registry:
            self._register_commands()

        logger.info("Agent orchestrator plugin initialized")

    async def start_monitor(self) -> None:
        """Start the activity monitor background task."""
        if self.activity_monitor and not self._monitor_task:
            self._monitor_task = asyncio.create_task(self.activity_monitor.start())
            logger.info("Activity monitor started")

    async def shutdown(self) -> None:
        """Cleanup on shutdown."""
        if self._monitor_task:
            self._monitor_task.cancel()
            try:
                await self._monitor_task
            except asyncio.CancelledError:
                logger.debug("Activity monitor task cancelled")
            except Exception as e:
                logger.warning(f"Error cancelling monitor task: {e}")

        # Unregister display filter
        DisplayFilterRegistry.unregister("agent_orchestrator")

        logger.info("Agent orchestrator plugin shutdown")

    # -------------------------------------------------------------------------
    # Command Registration
    # -------------------------------------------------------------------------

    def _register_commands(self):
        """Register subagent commands with the command registry."""
        from core.events.models import (
            CommandDefinition, CommandMode, CommandCategory, SubcommandInfo
        )

        # /subagent - manage agent sessions
        subagent_cmd = CommandDefinition(
            name="subagent",
            description="Manage agent sessions (list/status/capture/stop/message)",
            handler=self._handle_subagent_command,
            plugin_name=self.name,
            category=CommandCategory.CUSTOM,
            mode=CommandMode.INSTANT,
            aliases=["sa"],
            icon="[🤖]",
            subcommands=[
                SubcommandInfo("list", "", "List all active agents"),
                SubcommandInfo("status", "<name>", "Get status of specific agent"),
                SubcommandInfo("capture", "<name> [lines]", "Capture output from agent"),
                SubcommandInfo("stop", "<name>", "Stop an agent"),
                SubcommandInfo("message", "<name> <msg>", "Send message to agent"),
            ]
        )
        self.command_registry.register_command(subagent_cmd)

        self.logger.info("Subagent commands registered")

    async def _handle_subagent_command(self, command) -> "CommandResult":
        """Handle /subagent command with subcommands.

        Usage:
            /subagent list                    List all active agents
            /subagent status <name>           Get status of specific agent
            /subagent capture <name> [lines]  Capture output from agent
            /subagent stop <name>             Stop an agent
            /subagent message <name> <msg>    Send message to agent
        """
        from core.events.models import CommandResult

        args = command.args if command.args else []

        if not args:
            # No args - show list by default
            return await self._cmd_list([])

        subcommand = args[0].lower()

        if subcommand == "list":
            return await self._cmd_list(args[1:])
        elif subcommand == "status":
            return await self._cmd_status(args[1:])
        elif subcommand == "capture":
            return await self._cmd_capture(args[1:])
        elif subcommand == "stop":
            return await self._cmd_stop(args[1:])
        elif subcommand == "message":
            return await self._cmd_message(args[1:])
        elif subcommand == "help" or subcommand == "--help" or subcommand == "-h":
            return CommandResult(
                success=True,
                message=self._get_help_text(),
                display_type="info"
            )
        else:
            return CommandResult(
                success=False,
                message=f"Unknown subcommand: {subcommand}\n\n{self._get_help_text()}",
                display_type="error"
            )

    def _get_help_text(self) -> str:
        """Get help text for subagent command."""
        return """Agent Session Manager

Manage parallel kollab sub-agents spawned via XML commands.

Usage:
  /subagent list                    List all active agents
  /subagent status <name>           Get status of specific agent
  /subagent capture <name> [lines]  Capture output from agent (default: 50)
  /subagent stop <name>             Stop an agent
  /subagent message <name> <msg>    Send message to agent

Examples:
  /subagent list
  /subagent status researcher-1
  /subagent capture dev-agent 100
  /subagent stop researcher-1
  /subagent message dev-agent "continue with task 2"

Aliases: /sa"""

    async def _cmd_list(self, args: list) -> "CommandResult":
        """List all active agents.

        Args:
            args: Additional arguments (unused for list).

        Returns:
            CommandResult with agent list.
        """
        from core.events.models import CommandResult

        if not self.orchestrator:
            return CommandResult(
                success=False,
                message="Agent orchestrator not initialized",
                display_type="error"
            )

        agents = self.orchestrator.list_agents()

        if not agents:
            return CommandResult(
                success=True,
                message="No active agents. Use XML commands to spawn agents:\n  <agent><name><task>...</task></name></agent>",
                display_type="info"
            )

        lines = ["Active Agents:"]
        for agent in agents:
            lines.append(f"  {agent.name:<25} {agent.status:<12} {agent.duration}")

        return CommandResult(
            success=True,
            message="\n".join(lines),
            display_type="info"
        )

    async def _cmd_status(self, args: list) -> "CommandResult":
        """Get status of specific agent.

        Args:
            args: Agent name argument.

        Returns:
            CommandResult with agent status.
        """
        from core.events.models import CommandResult

        if not self.orchestrator:
            return CommandResult(
                success=False,
                message="Agent orchestrator not initialized",
                display_type="error"
            )

        if not args:
            return CommandResult(
                success=False,
                message="Usage: /subagent status <agent_name>",
                display_type="error"
            )

        agent_name = args[0]
        agent = self.orchestrator.get_agent(agent_name)

        if not agent:
            return CommandResult(
                success=False,
                message=f"Agent '{agent_name}' not found",
                display_type="error"
            )

        # Get status info
        import datetime
        created_at = datetime.datetime.fromtimestamp(agent.start_time).strftime("%Y-%m-%d %H:%M:%S")
        lines = [
            f"Agent: {agent.name}",
            f"Status: {agent.status}",
            f"Duration: {agent.duration}",
            f"Started: {created_at}",
        ]

        return CommandResult(
            success=True,
            message="\n".join(lines),
            display_type="info"
        )

    async def _cmd_capture(self, args: list) -> "CommandResult":
        """Capture output from agent.

        Args:
            args: Agent name and optional lines count.

        Returns:
            CommandResult with captured output.
        """
        from core.events.models import CommandResult

        if not self.orchestrator:
            return CommandResult(
                success=False,
                message="Agent orchestrator not initialized",
                display_type="error"
            )

        if not args:
            return CommandResult(
                success=False,
                message="Usage: /subagent capture <agent_name> [lines]",
                display_type="error"
            )

        agent_name = args[0]
        lines = 50  # default
        if len(args) > 1:
            try:
                lines = int(args[1])
            except ValueError:
                return CommandResult(
                    success=False,
                    message=f"Invalid line count: {args[1]}",
                    display_type="error"
                )

        output = self.orchestrator.capture_output(agent_name, lines)
        agent = self.orchestrator.get_agent(agent_name)

        if not agent:
            return CommandResult(
                success=False,
                message=f"Agent '{agent_name}' not found",
                display_type="error"
            )

        # Format output
        output_lines = output.strip().split("\n")
        preview = "\n".join(output_lines[-lines:])

        result = f"[capture: {agent_name} @ {agent.duration}, {len(output_lines)} lines]\n{preview}"

        return CommandResult(
            success=True,
            message=result,
            display_type="info"
        )

    async def _cmd_stop(self, args: list) -> "CommandResult":
        """Stop an agent.

        Args:
            args: Agent name argument.

        Returns:
            CommandResult with stop confirmation.
        """
        from core.events.models import CommandResult

        if not self.orchestrator:
            return CommandResult(
                success=False,
                message="Agent orchestrator not initialized",
                display_type="error"
            )

        if not args:
            return CommandResult(
                success=False,
                message="Usage: /subagent stop <agent_name>",
                display_type="error"
            )

        agent_name = args[0]

        # Untrack from activity monitor
        if self.activity_monitor:
            self.activity_monitor.untrack(agent_name)

        # Stop agent
        output, duration = await self.orchestrator.stop(agent_name)

        # Emit plugin-specific event
        if self.event_bus:
            await self.event_bus.emit_with_hooks(
                "agent_stopped",
                {"name": agent_name, "duration": duration, "output": output},
                "agent_orchestrator"
            )

        # Truncate output for display
        output_lines = output.strip().split("\n")
        if len(output_lines) > 10:
            output_preview = "\n".join(output_lines[-10:])
        else:
            output_preview = output.strip()

        result = f"[stopped: {agent_name} @ {duration}]\n{output_preview}"

        return CommandResult(
            success=True,
            message=result,
            display_type="success"
        )

    async def _cmd_message(self, args: list) -> "CommandResult":
        """Send message to agent.

        Args:
            args: Agent name and message content.

        Returns:
            CommandResult with message confirmation.
        """
        from core.events.models import CommandResult

        if not self.orchestrator:
            return CommandResult(
                success=False,
                message="Agent orchestrator not initialized",
                display_type="error"
            )

        if len(args) < 2:
            return CommandResult(
                success=False,
                message="Usage: /subagent message <agent_name> <message>",
                display_type="error"
            )

        agent_name = args[0]
        content = " ".join(args[1:])

        success = await self.orchestrator.message(agent_name, content)

        # Reset activity state so monitor waits for new activity
        if success and self.activity_monitor:
            self.activity_monitor.reset_agent_state(agent_name)

        if success:
            return CommandResult(
                success=True,
                message=f"Message sent to '{agent_name}'",
                display_type="success"
            )
        else:
            return CommandResult(
                success=False,
                message=f"Agent '{agent_name}' not found",
                display_type="error"
            )

    # -------------------------------------------------------------------------
    # Display Filter
    # -------------------------------------------------------------------------

    def _strip_orchestrator_xml(self, content: str, message_type: MessageType) -> str:
        """Strip orchestrator XML commands from content before display.

        These commands are displayed separately as tool indicators,
        so we strip them from the prose to avoid duplication.

        For USER messages, this strips <sys_msg> tags that contain agent
        orchestration instructions injected by the plugin.

        Args:
            content: Message content to filter.
            message_type: Type of message (ASSISTANT and USER messages filtered).

        Returns:
            Content with orchestrator XML stripped.
        """
        # Early exit - skip expensive regex if no orchestrator tags present
        if not _ORCH_QUICK_CHECK.search(content):
            return content

        # For user messages, strip <sys_msg> tags (plugin instruction injection)
        if message_type == MessageType.USER:
            content = _STRIP_SYS_MSG.sub("", content)

        # For assistant messages, strip orchestrator XML commands
        content = _STRIP_AGENT.sub("", content)
        content = _STRIP_STATUS.sub("", content)
        content = _STRIP_CAPTURE.sub("", content)
        content = _STRIP_STOP.sub("", content)
        content = _STRIP_MESSAGE.sub("", content)
        content = _STRIP_CLONE.sub("", content)
        content = _STRIP_TEAM.sub("", content)
        content = _STRIP_BROADCAST.sub("", content)

        return content

    # -------------------------------------------------------------------------
    # Hook Registration
    # -------------------------------------------------------------------------

    async def register_hooks(self) -> None:
        """Register hooks for LLM response processing and keyword triggers."""
        if not self.event_bus:
            logger.warning("No event bus available for hook registration")
            return

        # Hook for processing XML commands in LLM responses
        response_hook = Hook(
            name="agent_orchestrator_response",
            plugin_name=self.name,
            event_type=EventType.LLM_RESPONSE_POST,
            callback=self._on_llm_response,
            priority=HookPriority.POSTPROCESSING.value,
        )
        await self.event_bus.register_hook(response_hook)

        # Hook for keyword-triggered instruction injection
        keyword_hook = Hook(
            name="agent_orchestrator_keyword_trigger",
            plugin_name=self.name,
            event_type=EventType.USER_INPUT_PRE,
            callback=self._on_user_input,
            priority=HookPriority.PREPROCESSING.value,
        )
        await self.event_bus.register_hook(keyword_hook)

        logger.info("Registered agent orchestration hooks")

    # -------------------------------------------------------------------------
    # Default Config
    # -------------------------------------------------------------------------

    @staticmethod
    def get_default_config() -> Dict[str, Any]:
        """Get default configuration for this plugin."""
        return {
            "plugins": {
                "agent_orchestrator": {
                    "enabled": True,
                    "poll_interval": 2,
                    "idle_threshold": 3,
                    "capture_lines": 500,
                    "max_concurrent": 10,
                    # How to enable agent instructions for LLM:
                    # "startup" = inject into system prompt at startup
                    # "keyword" = inject when keywords detected in user input
                    # "disabled" = don't inject (LLM won't know about agents)
                    "enable_mode": "keyword",
                    # Trigger delay for keyword mode:
                    # "0" = inject once (first trigger only)
                    # "60s" = inject every 60 seconds
                    # "5m" = inject every 5 messages/turns
                    "trigger_delay": "0",
                }
            }
        }

    def get_system_prompt_addition(self) -> Optional[str]:
        """Get system prompt addition if enable_mode is 'startup'.

        Returns:
            Agent instructions string, or None if not startup mode.
        """
        if not self.config:
            return None

        enable_mode = self.config.get("plugins.agent_orchestrator.enable_mode", "keyword")
        if enable_mode == "startup":
            return AGENT_INSTRUCTIONS
        return None

    def _parse_trigger_delay(self, delay_str: str) -> tuple:
        """Parse trigger delay string.

        Args:
            delay_str: Delay string like "0", "60s", "5m"

        Returns:
            Tuple of (delay_type, delay_value) where:
            - ("once", 0) for "0"
            - ("seconds", N) for "Ns"
            - ("messages", N) for "Nm"
        """
        delay_str = delay_str.strip().lower()

        if delay_str == "0":
            return ("once", 0)

        if delay_str.endswith("s"):
            try:
                return ("seconds", int(delay_str[:-1]))
            except ValueError:
                return ("once", 0)

        if delay_str.endswith("m"):
            try:
                return ("messages", int(delay_str[:-1]))
            except ValueError:
                return ("once", 0)

        return ("once", 0)

    def _should_inject(self) -> bool:
        """Check if we should inject instructions based on trigger_delay.

        Returns:
            True if injection should happen.
        """
        import time

        trigger_delay = self.config.get("plugins.agent_orchestrator.trigger_delay", "0") if self.config else "0"
        delay_type, delay_value = self._parse_trigger_delay(trigger_delay)

        if delay_type == "once":
            # Only inject if never injected before
            return self._last_injection_time == 0.0

        elif delay_type == "seconds":
            # Inject if enough time has passed
            elapsed = time.time() - self._last_injection_time
            return elapsed >= delay_value

        elif delay_type == "messages":
            # Inject if enough messages have passed
            return self._message_count_since_injection >= delay_value

        return False

    async def _on_user_input(self, data: dict, event) -> dict:
        """Check for trigger keywords and inject instructions.

        Args:
            data: Event data with user input.
            event: Event object.

        Returns:
            Modified data with injected instructions.
        """
        logger.info(f"[AGENT_ORCH] _on_user_input called with data keys: {list(data.keys())}")
        context = data  # Alias for compatibility
        import time

        if not self.config:
            logger.info("[AGENT_ORCH] No config, returning early")
            return context

        enable_mode = self.config.get("plugins.agent_orchestrator.enable_mode", "keyword")
        if enable_mode != "keyword":
            return context

        # Increment message count for tracking
        self._message_count_since_injection += 1

        # The event data uses "message" key, not "input"
        user_input = context.get("message", context.get("input", "")).lower()
        logger.info(f"[AGENT_ORCH] User input: {user_input[:50]}...")

        # Check for trigger keywords
        triggered = any(kw in user_input for kw in TRIGGER_KEYWORDS)
        logger.info(f"[AGENT_ORCH] Triggered: {triggered}")

        if triggered and self._should_inject():
            # Inject instructions as sys_msg (hidden from user display)
            original_input = context.get("message", context.get("input", ""))
            injected = f"""<sys_msg>
{AGENT_INSTRUCTIONS}
</sys_msg>

{original_input}"""
            context["message"] = injected
            if "input" in context:
                context["input"] = injected

            # Update tracking
            self._last_injection_time = time.time()
            self._message_count_since_injection = 0

            logger.info("Injected agent orchestration instructions (keyword trigger)")

        return context

    # -------------------------------------------------------------------------
    # Core Logic
    # -------------------------------------------------------------------------

    async def _on_llm_response(self, data: dict, event) -> dict:
        """Process LLM response for agent XML commands.

        In interactive mode: spawn commands run in background, query commands sync.
        In pipe mode: all commands run synchronously to wait for completion.

        Args:
            data: Event data with response data.
            event: Event object.

        Returns:
            Modified data.
        """
        context = data  # Alias for compatibility
        response_text = context.get("response_text", "")
        if not response_text:
            response_text = context.get("content", "")

        if not response_text:
            return context

        # Parse XML commands from response
        commands = self.xml_parser.parse(response_text)

        if not commands:
            return context  # no agent commands

        logger.info(f"Found {len(commands)} agent command(s) in LLM response")

        # Check if we're in pipe mode
        pipe_mode = getattr(self.renderer, 'pipe_mode', False)

        # Split commands into blocking (spawn) and non-blocking (query)
        blocking_commands = []  # agent, message, clone, team, broadcast
        non_blocking_commands = []  # status, capture, stop

        for cmd in commands:
            if cmd.type in ("agent", "message", "clone", "team", "broadcast"):
                blocking_commands.append(cmd)
            else:
                non_blocking_commands.append(cmd)

        # Execute non-blocking commands immediately (status, capture, stop)
        results = []
        for cmd in non_blocking_commands:
            try:
                self._display_tool_indicator(cmd)
                result = await self._execute_command(cmd)
                if result:
                    results.append(result)
                    # Display result for capture commands
                    if cmd.type == "capture":
                        self._display_tool_result(cmd, result, is_error=False)
            except Exception as e:
                logger.error(f"Error executing agent command: {e}")
                results.append(f"[error: {e}]")
                self._display_tool_result(cmd, f"[error: {e}]", is_error=True)

        # Execute blocking commands
        if pipe_mode:
            # In pipe mode, wait for all commands to complete
            for cmd in blocking_commands:
                try:
                    self._display_tool_indicator(cmd)
                    result = await self._execute_command(cmd, wait=True)
                    if result:
                        results.append(result)
                except Exception as e:
                    logger.error(f"Error executing agent command: {e}")
                    results.append(f"[error: {e}]")
                    self._display_tool_result(cmd, f"[error: {e}]", is_error=True)
        else:
            # In interactive mode, run blocking commands in background
            for cmd in blocking_commands:
                self._display_tool_indicator(cmd)
                # Create background task for non-blocking execution
                asyncio.create_task(self._execute_command_background(cmd))
            logger.info(f"Spawned {len(blocking_commands)} background task(s)")

        # Inject results back to LLM
        if results:
            result_text = "\n".join(results)
            has_error = any(r.startswith("[error") for r in results)

            if self.message_injector:
                await self.message_injector.inject(
                    source="agent_orchestrator",
                    content=result_text,
                    trigger_llm=False,
                )

            if not has_error:
                context["force_continue"] = True
                logger.info("Forcing continuation for orchestrator result")

        return context

    async def _execute_command_background(self, cmd) -> None:
        """Execute command in background and inject result when complete.

        Args:
            cmd: ParsedCommand instance.
        """
        try:
            result = await self._execute_command(cmd)
            if result and self.message_injector:
                # Inject result back to conversation
                await self.message_injector.inject(
                    source="agent_orchestrator",
                    content=result,
                    trigger_llm=True,  # Auto-continue to notify LLM
                )
                logger.info(f"Background command {cmd.type} completed, result injected")
        except Exception as e:
            logger.error(f"Error in background command {cmd.type}: {e}")
            if self.message_injector:
                await self.message_injector.inject(
                    source="agent_orchestrator",
                    content=f"[error: {e}]",
                    trigger_llm=True,
                )

    def _display_tool_indicator(self, cmd) -> None:
        """Display tool call indicator like real tool calls.

        Args:
            cmd: Parsed command object.
        """
        if not self.renderer:
            return

        indicator = f"{ColorPalette.BRIGHT_LIME}⏺{ColorPalette.RESET}"

        if cmd.type == "agent":
            # Get agent names from the agents list
            names = [a.name for a in cmd.agents] if cmd.agents else []
            args = ", ".join(names) if names else ""
            tool_line = f"{indicator} spawn_agent({args})"
        elif cmd.type == "status":
            tool_line = f"{indicator} status()"
        elif cmd.type == "capture":
            tool_line = f"{indicator} capture({cmd.target}, {cmd.lines})"
        elif cmd.type == "stop":
            targets = ", ".join(cmd.targets) if cmd.targets else ""
            tool_line = f"{indicator} stop({targets})"
        elif cmd.type == "message":
            tool_line = f"{indicator} message({cmd.target})"
        elif cmd.type == "clone":
            names = [a.name for a in cmd.agents] if cmd.agents else []
            tool_line = f"{indicator} clone({names[0] if names else ''})"
        elif cmd.type == "team":
            tool_line = f"{indicator} spawn_team({cmd.lead})"
        elif cmd.type == "broadcast":
            tool_line = f"{indicator} broadcast({cmd.pattern})"
        else:
            tool_line = f"{indicator} {cmd.type}()"

        # Display using message coordinator
        self.renderer.message_coordinator.display_message_sequence([
            ("system", tool_line, {})
        ])

    def _display_tool_result(self, cmd, result: str, is_error: bool = False) -> None:
        """Display tool execution result like real tool calls.

        Args:
            cmd: Parsed command object.
            result: Result string from execution.
            is_error: Whether this is an error result.
        """
        if not self.renderer:
            return

        if is_error:
            result_line = f"\033[31m ▮ {result}\033[0m"
        else:
            # Format based on command type
            if cmd.type == "agent":
                result_line = f"\033[32m ▮ {result}\033[0m"
            elif cmd.type == "status":
                # Status returns multi-line, show summary
                line_count = result.count('\n') + 1
                result_line = f"\033[32m ▮ Retrieved status ({line_count} lines)\033[0m"
            elif cmd.type == "capture":
                # Capture returns full output with preview - display it
                result_line = f"\033[32m ▮\033[0m\n{result}"
            else:
                result_line = f"\033[32m ▮ {result}\033[0m"

        self.renderer.message_coordinator.display_message_sequence([
            ("system", result_line, {})
        ])

    async def _execute_command(self, cmd, wait: bool = False) -> str:
        """Execute a parsed agent command.

        Args:
            cmd: ParsedCommand instance.
            wait: For spawn commands, wait for initialization to complete.

        Returns:
            Result string.
        """
        if cmd.type == "agent":
            return await self._spawn_agents(cmd.agents, wait=wait)
        elif cmd.type == "message":
            return await self._message_agent(cmd.target, cmd.content)
        elif cmd.type == "stop":
            return await self._stop_agents(cmd.targets)
        elif cmd.type == "status":
            return await self._get_status()
        elif cmd.type == "capture":
            return await self._capture_output(cmd.target, cmd.lines)
        elif cmd.type == "clone":
            return await self._clone_agent(cmd.agents[0] if cmd.agents else None, wait=wait)
        elif cmd.type == "team":
            return await self._spawn_team(cmd.lead, cmd.workers, cmd.agents, wait=wait)
        elif cmd.type == "broadcast":
            return await self._broadcast(cmd.pattern, cmd.content)
        else:
            return f"[error: unknown command type '{cmd.type}']"

    async def _spawn_agents(self, agents: list, wait: bool = False) -> str:
        """Spawn multiple agents in parallel.

        Args:
            agents: List of AgentTask instances.
            wait: If True, wait for initialization to complete.

        Returns:
            Result string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        # Spawn all agents in parallel for better performance
        tasks = [
            self.orchestrator.spawn(name=agent.name, task=agent.task, files=agent.files, wait=wait)
            for agent in agents
        ]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        spawned = []
        for agent, result in zip(agents, results):
            # Check for success (True) and not an exception
            if result is True:
                spawned.append(agent.name)
                if self.activity_monitor:
                    self.activity_monitor.track(agent.name)

                # Emit plugin-specific event (string literal, not in core EventType)
                if self.event_bus:
                    await self.event_bus.emit_with_hooks(
                        "agent_spawned",
                        {"name": agent.name, "task": agent.task},
                        "agent_orchestrator"
                    )
            elif isinstance(result, Exception):
                logger.error(f"Failed to spawn agent {agent.name}: {result}")

        if spawned:
            return f"[spawned: {', '.join(spawned)}]"
        return "[error: no agents spawned]"

    async def _message_agent(self, target: str, content: str) -> str:
        """Send message to agent.

        Args:
            target: Agent name.
            content: Message content.

        Returns:
            Result string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        success = await self.orchestrator.message(target, content)

        # Reset activity state so monitor waits for new activity
        if success and self.activity_monitor:
            self.activity_monitor.reset_agent_state(target)

        if success:
            return f"[message sent: {target}]"
        return f"[error: agent {target} not found]"

    async def _stop_agents(self, targets: list) -> str:
        """Stop agents and capture final output.

        Args:
            targets: List of agent names to stop.

        Returns:
            Result string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        results = []

        for target in targets:
            if self.activity_monitor:
                self.activity_monitor.untrack(target)

            output, duration = await self.orchestrator.stop(target)

            # Emit plugin-specific event (string literal, not in core EventType)
            if self.event_bus:
                await self.event_bus.emit_with_hooks(
                    "agent_stopped",
                    {"name": target, "duration": duration, "output": output},
                    "agent_orchestrator"
                )

            # Truncate output for display
            output_lines = output.strip().split("\n")
            if len(output_lines) > 10:
                output_preview = "\n".join(output_lines[-10:])
            else:
                output_preview = output.strip()

            results.append(f"[stopped: {target} @ {duration}]\n{output_preview}")

        return "\n".join(results)

    async def _get_status(self) -> str:
        """Get status of all agents.

        Returns:
            Status string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        agents = self.orchestrator.list_agents()

        if not agents:
            return "[agents]\n  (none active)"

        lines = ["[agents]"]
        for agent in agents:
            lines.append(f"  {agent.name:<20} {agent.status:<10} {agent.duration}")

        return "\n".join(lines)

    async def _capture_output(self, target: str, lines: int) -> str:
        """Capture output from agent.

        Args:
            target: Agent name.
            lines: Number of lines to capture.

        Returns:
            Captured output string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        output = self.orchestrator.capture_output(target, lines)
        agent = self.orchestrator.get_agent(target)
        duration = agent.duration if agent else "?"

        # Extract last 10 lines for preview
        output_lines = output.strip().split("\n")
        preview_lines = output_lines[-10:] if len(output_lines) > 10 else output_lines
        preview = "\n".join(preview_lines)

        return f"[capture: {target} @ {duration}, {lines} lines]\n{preview}"

    async def _clone_agent(self, agent: AgentTask, wait: bool = False) -> str:
        """Clone agent with conversation context.

        Args:
            agent: AgentTask to clone.
            wait: If True, wait for initialization to complete.

        Returns:
            Result string.
        """
        if not agent:
            return "[error: no agent specified for clone]"

        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        # Export conversation
        conv_file = await self._export_conversation()
        if not conv_file:
            return "[error: could not export conversation for clone]"

        success = await self.orchestrator.spawn_clone(
            name=agent.name,
            task=agent.task,
            files=agent.files,
            conversation_file=conv_file,
            wait=wait,
        )

        if success:
            if self.activity_monitor:
                self.activity_monitor.track(agent.name)
            return f"[cloned: {agent.name} with conversation context]"
        return f"[error: failed to clone {agent.name}]"

    async def _spawn_team(self, lead: str, workers: int, agents: list, wait: bool = False) -> str:
        """Spawn team lead agent.

        Args:
            lead: Lead agent name.
            workers: Max number of workers.
            agents: List of agent tasks (should have one for the lead).
            wait: If True, wait for initialization to complete.

        Returns:
            Result string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        task = agents[0] if agents else AgentTask(name=lead, task="", files=[])

        success = await self.orchestrator.spawn_team_lead(
            lead_name=lead, max_workers=workers, task=task, wait=wait
        )

        if success:
            if self.activity_monitor:
                self.activity_monitor.track(lead)
            return f"[team spawned: {lead} (max {workers} workers)]"
        return f"[error: failed to spawn team {lead}]"

    async def _broadcast(self, pattern: str, content: str) -> str:
        """Broadcast message to agents matching pattern.

        Args:
            pattern: Glob pattern for matching agents.
            content: Message content.

        Returns:
            Result string.
        """
        if not self.orchestrator:
            return "[error: orchestrator not initialized]"

        targets = self.orchestrator.find_agents(pattern)
        count = 0

        for target in targets:
            if await self.orchestrator.message(target, content):
                count += 1
                # Reset activity state
                if self.activity_monitor:
                    self.activity_monitor.reset_agent_state(target)

        return f"[broadcast: sent to {count} agents matching '{pattern}']"

    # -------------------------------------------------------------------------
    # Callbacks
    # -------------------------------------------------------------------------

    async def _on_agent_complete(
        self, name: str, duration: str, output: str
    ) -> None:
        """Called when activity monitor detects agent completion.

        Args:
            name: Agent name.
            duration: Duration string.
            output: Captured output.
        """
        logger.info(f"Agent completed: {name} @ {duration}")

        # Emit plugin-specific event (string literal, not in core EventType)
        if self.event_bus:
            await self.event_bus.emit_with_hooks(
                "agent_completed",
                {"name": name, "duration": duration, "output": output},
                "agent_orchestrator"
            )

        # Inject completion message
        if self.message_injector:
            # Truncate output for summary
            output_lines = output.strip().split("\n")
            if len(output_lines) > 20:
                summary = "\n".join(output_lines[-20:])
            else:
                summary = output.strip()

            await self.message_injector.inject(
                source=name,
                content=f"[done: {name} @ {duration}]\n{summary}",
                trigger_llm=True,  # auto-continue conversation
            )

    async def _export_conversation(self) -> Optional[str]:
        """Export current conversation to temp file.

        Returns:
            Path to temp file, or None on error.
        """
        if not self.conversation_manager:
            return None

        try:
            messages = self.conversation_manager.get_messages()

            fd, path = tempfile.mkstemp(suffix=".json", prefix="conv-")
            with open(path, "w") as f:
                json.dump(messages, f)

            return path
        except Exception as e:
            logger.error(f"Failed to export conversation: {e}")
            return None
