"""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,
        )

        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")

    # -------------------------------------------------------------------------
    # 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.

        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")

        results = []

        for cmd in commands:
            try:
                # Display tool indicator before execution (like real tool calls)
                self._display_tool_indicator(cmd)

                result = await self._execute_command(cmd)
                if result:
                    results.append(result)
                    # Display result for capture commands (show preview to user)
                    # Other commands only show indicator - result injected to conversation
                    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}]")
                # Only show errors to the user
                self._display_tool_result(cmd, f"[error: {e}]", is_error=True)

        # Inject results back to LLM (like tool call results)
        if results:
            result_text = "\n".join(results)

            # Check if any result is an error (don't continue on errors to prevent loops)
            has_error = any(r.startswith("[error") or "already exists" in r.lower() for r in results)

            # Inject to conversation history for LLM context
            if self.message_injector:
                await self.message_injector.inject(
                    source="agent_orchestrator",
                    content=result_text,
                    trigger_llm=False,  # we'll force continue via context flag
                )

            # Force continuation for all successful results (spawn, status, capture, etc.)
            # LLM will respond naturally to the tool result
            # Only skip continuation on errors to prevent infinite loops
            if not has_error:
                context["force_continue"] = True
                logger.info("Forcing continuation for orchestrator result")

        return context

    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) -> str:
        """Execute a parsed agent command.

        Args:
            cmd: ParsedCommand instance.

        Returns:
            Result string.
        """
        if cmd.type == "agent":
            return await self._spawn_agents(cmd.agents)
        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)
        elif cmd.type == "team":
            return await self._spawn_team(cmd.lead, cmd.workers, cmd.agents)
        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) -> str:
        """Spawn multiple agents.

        Args:
            agents: List of AgentTask instances.

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

        spawned = []

        for agent in agents:
            success = await self.orchestrator.spawn(
                name=agent.name, task=agent.task, files=agent.files
            )
            if success:
                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"
                    )

        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) -> str:
        """Clone agent with conversation context.

        Args:
            agent: AgentTask to clone.

        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,
        )

        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) -> 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).

        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
        )

        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
