"""
WorkspaceExecutionEngine - Production-hard execution engine for multi-agent workspaces.

This module provides the WorkspaceExecutionEngine class that:
- Executes agents using event-driven scheduling with deterministic message IDs
- Prevents dropped messages through ID-based matching
- Enforces recursion safety (max workspaces, depth limits, per-agent throttling)
- Protects against infinite loops and unbounded execution
- Validates agent outputs strictly
- Maintains complete transcripts for debugging
"""

from __future__ import annotations

import json
import logging
import asyncio
from typing import Any, Optional
from datetime import datetime

from synqed.agent import AgentLogicContext
from synqed.memory import InboxMessage, AgentMemory
from synqed.workspace_manager import Workspace, WorkspaceManager
from synqed.planner import PlannerLLM
from synqed.scheduler import EventScheduler, AgentEvent

logger = logging.getLogger(__name__)


class Context(AgentLogicContext):
    """
    Context object passed to agent logic functions during workspace execution.
    
    Extends AgentLogicContext with workspace-specific information and event details.
    """
    
    def __init__(
        self,
        agent_name: str,
        workspace: Workspace,
        workspace_id: str,
        messages: list[InboxMessage],
        memory: AgentMemory,
        default_target: Optional[str] = None,
        event_trigger: Optional[str] = None,
        event_payload: Optional[dict] = None,
    ):
        """Initialize the context."""
        super().__init__(memory=memory, default_target=default_target)
        self.agent_name = agent_name
        self.workspace = workspace
        self.workspace_id = workspace_id
        self.messages = messages
        self.event_trigger = event_trigger
        self.event_payload = event_payload or {}
    
    def build_response(self, target: str, content: str) -> dict[str, str]:
        """Build a response dictionary. Delegates to parent AgentLogicContext.build_response."""
        return super().build_response(target, content)
    
    def __repr__(self) -> str:
        """String representation."""
        return f"Context(agent='{self.agent_name}', workspace='{self.workspace_id}', messages={len(self.messages)})"


class WorkspaceExecutionEngine:
    """
    Production-hard execution engine for multi-agent workspaces.
    
    Features:
    - Deterministic message processing with unique IDs
    - Zero dropped messages through ID-based matching
    - Recursion safety (max workspaces, depth limits, throttling)
    - Event loop protection (max events per workspace/cycle)
    - Infinite loop detection and prevention
    - Strict agent output validation
    - Complete transcript tracking
    """
    
    # Global limits
    max_total_workspaces = 50
    max_workspace_depth = 8
    max_subteam_requests_per_agent = 3
    max_events_per_workspace = 2000
    max_events_per_cycle = 50
    fatal_cycle_threshold = 5  # Cycles with no output and non-empty queue
    
    def __init__(
        self,
        planner: PlannerLLM,
        workspace_manager: WorkspaceManager,
    ):
        """
        Initialize the workspace execution engine.
        
        Args:
            planner: PlannerLLM instance for creating subteam subtrees
            workspace_manager: WorkspaceManager instance for workspace operations
        """
        self.planner = planner
        self.workspace_manager = workspace_manager
        self._total_workspaces_created = 0
        # Global workspace execution queue for asynchronous hierarchical execution
        self.global_workspace_queue: asyncio.Queue[str] = asyncio.Queue()
        # Track which workspaces are currently executing to prevent re-entry
        self._running_workspaces: set[str] = set()
        # Track which agent requested each child workspace (child_id -> requesting_agent_name)
        self._subteam_requesters: dict[str, str] = {}
    
    def schedule_workspace(self, workspace_id: str) -> None:
        """
        Schedule a workspace for execution in the global queue.
        
        This method enqueues a workspace ID for later execution. Workspaces
        cannot be enqueued if they are already running.
        
        Args:
            workspace_id: ID of workspace to schedule
        """
        workspace = self.workspace_manager.get_workspace(workspace_id)
        
        # Prevent re-entry: skip if already running
        if workspace.is_running or workspace_id in self._running_workspaces:
            logger.debug(f"Workspace {workspace_id} is already running, skipping enqueue")
            return
        
        # Enqueue for execution
        self.global_workspace_queue.put_nowait(workspace_id)
        logger.debug(f"Scheduled workspace {workspace_id} for execution")
    
    async def run_global_scheduler(self) -> None:
        """
        Process all workspaces in the global execution queue.
        
        This method continuously processes workspaces from the queue until
        the queue is empty. Each workspace runs independently without recursion.
        Workspaces may schedule child workspaces during execution, which will
        be processed in subsequent iterations.
        """
        logger.info("Starting global workspace scheduler")
        
        # Continue processing until queue is empty
        # New workspaces may be scheduled during execution
        processed_count = 0
        max_iterations = 1000  # Safety limit to prevent infinite loops
        
        for iteration in range(max_iterations):
            if self.global_workspace_queue.empty():
                break
            
            # Process all workspaces currently in queue
            batch_size = self.global_workspace_queue.qsize()
            for _ in range(batch_size):
                try:
                    workspace_id = self.global_workspace_queue.get_nowait()
                    await self.run_workspace(workspace_id=workspace_id, max_cycles=20)
                    processed_count += 1
                except asyncio.QueueEmpty:
                    break
                except Exception as e:
                    logger.error(f"Error executing workspace {workspace_id} from queue: {e}")
        
        if iteration >= max_iterations - 1:
            logger.warning(f"Global scheduler reached max iterations ({max_iterations})")
        
        logger.info(f"Global workspace scheduler completed. Processed {processed_count} workspaces.")
    
    async def run(self, root_workspace_id: str) -> None:
        """
        Top-level entrypoint for executing a root workspace and all its children.
        
        This method schedules the root workspace and then runs the global scheduler
        to process all workspaces (including children) asynchronously.
        
        Args:
            root_workspace_id: ID of the root workspace to execute
        """
        logger.info(f"Starting execution of root workspace {root_workspace_id}")
        self.schedule_workspace(root_workspace_id)
        await self.run_global_scheduler()
    
    async def run_workspace(
        self,
        workspace_id: str,
        max_cycles: int = 20,
    ) -> None:
        """
        Execute a workspace's agents using event-driven scheduling.
        
        This method executes a workspace independently without recursion.
        When a workspace completes, it sends subteam_result messages to its
        parent workspace if it has one.
        
        Args:
            workspace_id: ID of workspace to execute
            max_cycles: Maximum number of event processing cycles to execute
        """
        workspace = self.workspace_manager.get_workspace(workspace_id)
        
        # Prevent re-entry
        if workspace.is_running or workspace_id in self._running_workspaces:
            logger.warning(f"Workspace {workspace_id} is already running, skipping execution")
            return
        
        # Mark as running
        workspace.is_running = True
        self._running_workspaces.add(workspace_id)
        
        try:
            logger.info(f"Starting execution of workspace {workspace_id} (depth={workspace.depth}, max_cycles={max_cycles})")
            
            # Create event scheduler for this workspace
            scheduler = EventScheduler()
        
            # Schedule startup events for all agents (only on first execution)
            if not workspace.has_started:
                logger.info(f"Scheduling startup events for {len(workspace.agents)} agents")
                for agent_name, agent in workspace.agents.items():
                    # Route a system startup message via workspace
                    msg_id = await workspace.route_message(
                        sender="SYSTEM",
                        recipient=agent_name,
                        content="[startup]",
                        manager=self.workspace_manager,
                    )
                    
                    logger.info(f"Scheduled startup event for {agent_name} with message_id={msg_id}")
                    # Schedule startup event carrying that message_id
                    scheduler.schedule_event_dedup(AgentEvent(
                        agent_name=agent_name,
                        trigger="startup",
                        payload={"message_id": msg_id},
                    ))
                workspace.has_started = True
            
            # Check for unprocessed messages (e.g., subteam_result from child workspaces)
            # and schedule message events for them
            for agent_name, agent in workspace.agents.items():
                unprocessed_messages = agent.memory.get_unprocessed_messages()
                logger.info(
                    f"Agent {agent_name} has {len(unprocessed_messages)} unprocessed messages"
                )
                for message in unprocessed_messages:
                    # Determine trigger type based on message content
                    trigger = "message"
                    if message.content.startswith("[subteam_result]"):
                        trigger = "subteam_result"
                    
                    logger.info(
                        f"Scheduling {trigger} event for {agent_name} with message_id={message.message_id}"
                    )
                    # Schedule event for unprocessed messages
                    scheduler.schedule_event_dedup(AgentEvent(
                        agent_name=agent_name,
                        trigger=trigger,
                        payload={"message_id": message.message_id},
                    ))
            
            cycle = 0
            consecutive_no_output_cycles = 0
            total_events_processed = 0
            
            while scheduler.has_pending_events() and cycle < max_cycles:
                # Event loop protection: max events per workspace
                if total_events_processed >= self.max_events_per_workspace:
                    logger.error(
                        f"Workspace {workspace_id} stopped: exceeded max events per workspace "
                        f"({self.max_events_per_workspace}). Possible infinite loop."
                    )
                    # Add error transcript entry
                    self._add_error_transcript_entry(
                        workspace, workspace_id, "max_events_exceeded",
                        f"Stopped after {total_events_processed} events"
                    )
                    break
                
                cycle += 1
                logger.info(
                    f"Workspace {workspace_id}, cycle {cycle}/{max_cycles}, "
                    f"pending events: {scheduler.get_queue_size()}, "
                    f"total processed: {total_events_processed}"
                )
                
                cycle_had_output = False
                events_processed_this_cycle = 0
                
                # Process events with per-cycle throttle
                while (scheduler.has_pending_events() and 
                       events_processed_this_cycle < self.max_events_per_cycle):
                    
                    event = scheduler.pop_next_event()
                    if event is None:
                        break
                    
                    events_processed_this_cycle += 1
                    total_events_processed += 1
                    agent_name = event.agent_name
                    
                    logger.info(
                        f"Processing event: agent={agent_name}, trigger={event.trigger}, "
                        f"message_id={event.payload.get('message_id', 'N/A')}"
                    )
                    
                    # Strict validation for event.trigger
                    valid_triggers = {"startup", "message", "subteam_result"}
                    if event.trigger not in valid_triggers:
                        logger.error(
                            f"Invalid event trigger '{event.trigger}' for agent '{agent_name}' "
                            f"in workspace {workspace_id}. Valid triggers: {valid_triggers}. Skipping."
                        )
                        continue
                    
                    # Verify agent exists
                    if agent_name not in workspace.agents:
                        logger.warning(f"Event for unknown agent '{agent_name}' in workspace {workspace_id}")
                        continue
                    
                    agent = workspace.agents[agent_name]
                    
                    # Message-like events must always carry a message_id that matches AgentMemory
                    message_like_triggers = {"startup", "message", "subteam_result"}
                    message_id: Optional[str] = None
                    
                    if event.trigger in message_like_triggers:
                        message_id = event.payload.get("message_id")
                        if not message_id:
                            logger.warning(
                                f"{event.trigger} event missing message_id for agent {agent_name} "
                                f"in workspace {workspace_id}"
                            )
                            continue
                        
                        # Fetch exact message by id
                        message = agent.memory.get_message_by_id(message_id)
                        if message is None:
                            logger.debug(
                                f"message {message_id} not found for agent {agent_name} in workspace {workspace_id}, skipping"
                            )
                            continue
                        
                        # Check if already processed
                        if agent.memory.is_message_processed(message_id):
                            logger.debug(
                                f"message {message_id} already processed for agent {agent_name} in workspace {workspace_id}, skipping"
                            )
                            continue
                    
                    # Get messages for context
                    messages = agent.memory.get_messages()
                    
                    # Create context with event trigger and payload
                    context = Context(
                        agent_name=agent_name,
                        workspace=workspace,
                        workspace_id=workspace_id,
                        messages=messages[-10:] if messages else [],
                        memory=agent.memory,
                        default_target=agent.default_target,
                        event_trigger=event.trigger,
                        event_payload=event.payload,
                    )
                    
                    try:
                        # ExecutionEngine is the only component allowed to route messages;
                        # agent logic may not use routers. All routing must go through workspace.route_message.
                        # Agent.process enforces router isolation and structured return format.
                        logger.info(f"Calling agent.process() for {agent_name}")
                        result = await agent.process(context)
                        logger.info(f"Agent {agent_name} returned: {result}")
                        
                        # Mark message as processed after successful logic execution
                        if message_id:
                            agent.memory.mark_message_processed(message_id)
                        
                        # Hardened agent output validation
                        agent_response = self._validate_and_normalize_response(
                            result, agent_name, workspace
                        )
                        
                        if agent_response is None:
                            continue  # Invalid response, skip
                        
                        # Handle USER messages: route but do not schedule events
                        if agent_response["send_to"] == "USER":
                            await workspace.route_message(
                                sender=agent_name,
                                recipient="USER",
                                content=agent_response["content"],
                                manager=self.workspace_manager,
                            )
                            cycle_had_output = True
                            continue
                        
                        # Check for subteam request
                        subteam_handled = await self._handle_subteam_request(
                            workspace=workspace,
                            workspace_id=workspace_id,
                            agent_name=agent_name,
                            response=agent_response,
                            scheduler=scheduler,  # Only used for error cases
                        )
                        
                        if subteam_handled:
                            cycle_had_output = True
                            continue
                        
                        # Check for infinite self-message loops
                        if self._is_self_message_loop(
                            agent, agent_response["send_to"], agent_name, agent_response["content"]
                        ):
                            logger.warning(
                                f"Blocked self-message loop for agent {agent_name} in workspace {workspace_id}"
                            )
                            self._add_error_transcript_entry(
                                workspace, workspace_id, "self_message_loop_blocked",
                                f"Agent {agent_name} attempted self-message loop"
                            )
                            continue
                        
                        # Safety check: no events for nonexistent agents
                        if agent_response["send_to"] not in workspace.agents and agent_response["send_to"] != "USER":
                            agent_response["send_to"] = "USER"
                        
                        # Route message with deterministic ID
                        message_id = await workspace.route_message(
                            sender=agent_name,
                            recipient=agent_response["send_to"],
                            content=agent_response["content"],
                            manager=self.workspace_manager,
                        )
                        
                        if message_id:
                            # Schedule message event with message_id
                            scheduler.schedule_event_dedup(AgentEvent(
                                agent_name=agent_response["send_to"],
                                trigger="message",
                                payload={"message_id": message_id}
                            ))
                            
                            cycle_had_output = True
                            logger.debug(
                                f"Agent {agent_name} routed message {message_id} to {agent_response['send_to']} "
                                f"in workspace {workspace_id}"
                            )
                    
                    except Exception as e:
                        logger.error(f"Error executing agent {agent_name} in workspace {workspace_id}: {e}")
                        continue
                
                # Fatal-cycle detection: no output for N cycles with non-empty queue
                if cycle_had_output:
                    consecutive_no_output_cycles = 0
                else:
                    consecutive_no_output_cycles += 1
                    if (consecutive_no_output_cycles >= self.fatal_cycle_threshold and 
                        scheduler.has_pending_events()):
                        logger.error(
                            f"Workspace {workspace_id} killed: stuck (no output for "
                            f"{consecutive_no_output_cycles} cycles with {scheduler.get_queue_size()} pending events)"
                        )
                        self._add_error_transcript_entry(
                            workspace, workspace_id, "fatal_cycle_detected",
                            f"Killed after {consecutive_no_output_cycles} cycles with no output"
                        )
                        break
            
            logger.info(
                f"Completed execution of workspace {workspace_id} after {cycle} cycles. "
                f"Processed {total_events_processed} events. Stats: {scheduler.get_stats()}"
            )
            
            # If this workspace has a parent, send subteam_result message
            # Note: We don't have access to parent's scheduler here, so we'll route the message
            # and schedule the parent workspace. The parent will pick up the message when it runs.
            if workspace.parent_id:
                await self._send_subteam_result_to_parent(workspace, workspace_id)
        finally:
            # Mark as not running
            workspace.is_running = False
            self._running_workspaces.discard(workspace_id)
    
    async def _send_subteam_result_to_parent(
        self,
        child_workspace: Workspace,
        child_workspace_id: str,
    ) -> None:
        """
        Send subteam_result message to parent workspace when child completes.
        
        This method is called when a child workspace finishes execution.
        It creates a subteam_result message and routes it to the requesting
        agent in the parent workspace via normal message routing. The parent
        workspace will pick up this message when it runs next and schedule
        a subteam_result event automatically (via unprocessed message detection).
        
        Args:
            child_workspace: The child workspace that just completed
            child_workspace_id: ID of the child workspace
        """
        if not child_workspace.parent_id:
            return
        
        try:
            parent_workspace = self.workspace_manager.get_workspace(child_workspace.parent_id)
            
            # Find which agent in parent requested this subteam
            requesting_agent = self._subteam_requesters.get(child_workspace_id)
            
            if not requesting_agent:
                # Fallback: try to find agent with subteam requests
                for agent_name, count in parent_workspace.subteam_requests.items():
                    if count > 0:
                        requesting_agent = agent_name
                        break
                
                if not requesting_agent:
                    # Final fallback: send to first agent in parent
                    if parent_workspace.agents:
                        requesting_agent = list(parent_workspace.agents.keys())[0]
                    else:
                        logger.warning(f"No agent found in parent workspace {child_workspace.parent_id} for subteam_result")
                        return
            
            # Fetch final transcript from child workspace
            child_transcript = child_workspace.router.get_transcript()
            result_message = ""
            
            if child_transcript:
                # Only include the last message
                last_entry = child_transcript[-1]
                result_message = last_entry.get("content", "")[:500]  # Truncate
            else:
                result_message = f"Subteam {child_workspace_id} completed"
            
            # Build a canonical payload for the result
            payload = {
                "child_workspace_id": child_workspace_id,
                "result_message": result_message,
            }
            
            # Create a system message to the requesting agent with the subteam result
            # The message content starts with [subteam_result] so it will be detected
            # as a subteam_result trigger when the parent workspace processes unprocessed messages
            msg_id = await parent_workspace.route_message(
                sender="SYSTEM",
                recipient=requesting_agent,
                content=f"[subteam_result]{json.dumps(payload, sort_keys=True)}",
                manager=self.workspace_manager,
            )
            
            logger.info(
                f"Child workspace {child_workspace_id} sent subteam_result message {msg_id} to parent "
                f"{child_workspace.parent_id}, agent {requesting_agent}"
            )
            
            # Schedule parent workspace to process the subteam_result message
            # When parent runs, it will detect the unprocessed [subteam_result] message
            # and schedule a subteam_result event automatically
            self.schedule_workspace(child_workspace.parent_id)
        
        except Exception as e:
            logger.error(f"Error sending subteam_result to parent: {e}")
    
    def _validate_and_normalize_response(
        self,
        result: dict[str, str],
        agent_name: str,
        workspace: Workspace,
    ) -> Optional[dict[str, str]]:
        """
        Validate and normalize agent response with strict rules.
        
        Agent.process() guarantees a dict with "send_to" and "content", so this method
        only validates fields, handles nonexistent send_to by redirecting to USER,
        truncates overly long content, and ignores empty content.
        
        Args:
            result: dict[str, str] from Agent.process() with "send_to" and "content"
            agent_name: Name of the agent for logging
            workspace: Workspace instance for agent validation
            
        Returns:
            dict[str, str] with "send_to" and "content" if valid, None if invalid
        """
        # Validate required fields exist
        if "send_to" not in result or "content" not in result:
            logger.warning(f"Agent {agent_name} returned invalid response structure (missing send_to or content)")
            return None
        
        send_to = result["send_to"]
        content = result.get("content", "")
        
        # Handle nonexistent send_to by redirecting to USER
        if send_to not in workspace.agents and send_to != "USER":
            logger.warning(
                f"Agent {agent_name} sent to nonexistent agent '{send_to}', converting to USER"
            )
            send_to = "USER"
        
        # Truncate content if too long
        if len(content) > 10000:
            logger.warning(f"Agent {agent_name} response content truncated from {len(content)} to 10000 chars")
            content = content[:10000]
        
        # Ignore empty content
        if not content.strip():
            logger.debug(f"Agent {agent_name} returned empty content, ignoring")
            return None
        
        return {"send_to": send_to, "content": content}
    
    def _is_self_message_loop(
        self,
        agent: Any,
        send_to: str,
        agent_name: str,
        content: str,
    ) -> bool:
        """
        Detect infinite self-message loops.
        
        Returns True if:
        - send_to == sender AND
        - last 3 messages were self-targeted with same content
        """
        if send_to != agent_name:
            return False
        
        messages = agent.memory.get_last_n_messages(3)
        if len(messages) < 3:
            return False
        
        # Check if last 3 messages were self-targeted with same content
        self_targeted_same_content = sum(
            1 for msg in messages 
            if msg.from_agent == agent_name and msg.content == content
        )
        
        return self_targeted_same_content >= 3
    
    def _add_error_transcript_entry(
        self,
        workspace: Workspace,
        workspace_id: str,
        error_type: str,
        message: str,
    ) -> None:
        """Add an error entry to workspace transcript."""
        entry = {
            "timestamp": datetime.now().isoformat(),
            "workspace_id": workspace_id,
            "from": "SYSTEM",
            "to": "ERROR",
            "message_id": f"error-{datetime.now().isoformat()}",
            "content": f"[{error_type}] {message}",
        }
        workspace.router.add_transcript_entry(entry)
    
    async def _handle_subteam_request(
        self,
        workspace: Workspace,
        workspace_id: str,
        agent_name: str,
        response: dict[str, str],
        scheduler: EventScheduler,
    ) -> bool:
        """
        Handle a subteam request with recursion safety checks.
        
        The scheduler parameter is only used for error cases. The success path
        does not schedule any events - subteam results are handled later in
        _send_subteam_result_to_parent.
        
        Returns:
            True if subteam request was handled, False otherwise
        """
        # Parse request JSON from response content
        try:
            request_json = json.loads(response["content"])
        except (json.JSONDecodeError, TypeError, KeyError):
            return False
        
        if not isinstance(request_json, dict):
            return False
        
        if request_json.get("action") != "request_subteam":
            return False
        
        logger.info(f"Agent {agent_name} in workspace {workspace_id} requested subteam")
        
        # Recursion safety checks
        # 1. Global workspace limit
        if self._total_workspaces_created >= self.max_total_workspaces:
            logger.error(
                f"Subteam creation denied: max total workspaces ({self.max_total_workspaces}) exceeded"
            )
            await self._schedule_subteam_error_result(
                workspace, scheduler, agent_name, "max_workspaces_exceeded"
            )
            return True
        
        # 2. Depth limit
        if workspace.depth >= self.max_workspace_depth:
            logger.error(
                f"Subteam creation denied: max depth ({self.max_workspace_depth}) reached "
                f"for workspace {workspace_id}"
            )
            await self._schedule_subteam_error_result(
                workspace, scheduler, agent_name, "max_depth_exceeded"
            )
            return True
        
        # 3. Per-agent throttling
        agent_request_count = workspace.subteam_requests.get(agent_name, 0)
        if agent_request_count >= self.max_subteam_requests_per_agent:
            logger.error(
                f"Subteam creation denied: agent {agent_name} exceeded max requests "
                f"({self.max_subteam_requests_per_agent})"
            )
            await self._schedule_subteam_error_result(
                workspace, scheduler, agent_name, "max_requests_per_agent_exceeded"
            )
            return True
        
        try:
            # Ensure requesting_agent is set
            if "requesting_agent" not in request_json:
                request_json["requesting_agent"] = agent_name
            
            # Create subtree
            subtree_root = await self.planner.create_subteam_subtree(request_json)
            
            # Create child workspace
            child_workspace = await self.workspace_manager.create_workspace(
                task_tree_node=subtree_root,
                parent_workspace_id=workspace_id,
            )
            
            self._total_workspaces_created += 1
            
            # Link subteam
            self.workspace_manager.link_subteam(
                parent_workspace_id=workspace_id,
                subteam_workspace_id=child_workspace.workspace_id,
            )
            
            # Increment agent request count
            workspace.subteam_requests[agent_name] = agent_request_count + 1
            
            # Track which agent requested this child workspace
            self._subteam_requesters[child_workspace.workspace_id] = agent_name
            
            logger.info(
                f"Created child workspace {child_workspace.workspace_id} "
                f"(depth={child_workspace.depth}) for subteam request from {agent_name}"
            )
            
            # Schedule child workspace for asynchronous execution (no recursion)
            # The child will send subteam_result to parent when it completes
            self.schedule_workspace(child_workspace.workspace_id)
            
            logger.info(
                f"Scheduled child workspace {child_workspace.workspace_id} for execution. "
                f"Parent {workspace_id} will receive subteam_result when child completes."
            )
            
            # Return immediately - parent continues execution
            # Child workspace will send subteam_result message to parent when it finishes
            return True
            
        except Exception as e:
            logger.error(f"Error handling subteam request from {agent_name}: {e}")
            await self._schedule_subteam_error_result(
                workspace, scheduler, agent_name, f"error: {str(e)}"
            )
            return True
    
    async def _schedule_subteam_error_result(
        self,
        workspace: Workspace,
        scheduler: EventScheduler,
        agent_name: str,
        error_reason: str,
    ) -> None:
        """
        Schedule a subteam_result event pointing to an error message in memory.
        """
        payload = {
            "error": "subteam_limit_reached",
            "reason": error_reason,
        }
        
        # Create error message in agent memory via workspace.route_message
        msg_id = await workspace.route_message(
            sender="SYSTEM",
            recipient=agent_name,
            content=f"[subteam_result]{json.dumps(payload, sort_keys=True)}",
            manager=self.workspace_manager,
        )
        
        # Schedule the event with payload={"message_id": msg_id}
        scheduler.schedule_event_dedup(AgentEvent(
            agent_name=agent_name,
            trigger="subteam_result",
            payload={"message_id": msg_id},
        ))
