"""ExpertTeam - 专家团队容器（hub-and-spoke 模式）

管理 Expert 生命周期、团队状态和事件广播，
是 Expert Team hub-and-spoke 协作模式的中央协调点。

简化说明（U3）：
- 移除 CollaborationPlan 依赖（Lead Expert 自主分解任务）
- 移除跨阶段状态共享（Lead Expert 持有所有状态）
- 保留 handoff_transport 用于事件广播（不再用于 Agent 间通信）
- 保留 workspace 用于输出保存（不再用于跨阶段状态共享）
"""

from __future__ import annotations

import asyncio
import enum
import logging
import time
import uuid

from .config import ExpertConfig
from .expert import Expert
from .registry import ExpertTemplateRegistry
from ..core.handoff_transport import InProcessHandoffTransport
from ..core.shared_workspace import SharedWorkspace
from ..core.agent_pool import AgentPool

logger = logging.getLogger(__name__)


class TeamStatus(str, enum.Enum):
    """ExpertTeam lifecycle states.

    流水线模式生命周期：
    FORMING → PLANNING → EXECUTING → SYNTHESIZING → COMPLETED → DISSOLVED

    PLANNING 状态在 Lead Expert 分解任务为阶段时设置（KTD6），
    与前端 IExpertTeamState.status 的 'planning' 值对齐。
    """

    FORMING = "forming"
    PLANNING = "planning"
    EXECUTING = "executing"
    SYNTHESIZING = "synthesizing"
    COMPLETED = "completed"
    DISSOLVED = "dissolved"


class ExpertTeam:
    """Container managing a team of Experts in hub-and-spoke mode.

    In hub-and-spoke mode:
    - Lead Expert (hub) receives the task and decomposes it
    - Member Experts (spokes) execute subtasks in parallel
    - Lead Expert synthesizes the final result
    - No inter-agent communication (Lead Expert holds all state)
    """

    def __init__(
        self,
        team_id: str | None = None,
        workspace: SharedWorkspace | None = None,
        pool: AgentPool | None = None,
        template_registry: ExpertTemplateRegistry | None = None,
        redis_client: object | None = None,
    ):
        self.team_id = team_id or str(uuid.uuid4())
        # U4: Accept redis_client for SharedWorkspace state offloading.
        # If workspace is explicitly provided, redis_client is ignored.
        self._workspace = workspace or SharedWorkspace(redis_client=redis_client)
        self._pool = pool
        self._template_registry = template_registry or ExpertTemplateRegistry()
        self._handoff_transport = InProcessHandoffTransport()
        self._experts: dict[str, Expert] = {}
        self._lead_expert_name: str | None = None
        self._status = TeamStatus.FORMING
        self._team_channel = f"team:{self.team_id}"
        self._orchestrator_task: asyncio.Task | None = None
        # U4: User intervention queue — bounded to prevent unbounded growth.
        # Consumed by TeamOrchestrator at phase boundaries.
        self._interventions: asyncio.Queue[str] = asyncio.Queue(maxsize=64)

    @property
    def status(self) -> TeamStatus:
        return self._status

    @property
    def lead_expert(self) -> Expert | None:
        if self._lead_expert_name:
            return self._experts.get(self._lead_expert_name)
        return None

    @property
    def experts(self) -> list[Expert]:
        return list(self._experts.values())

    @property
    def active_experts(self) -> list[Expert]:
        return [e for e in self._experts.values() if e.is_active]

    @property
    def workspace(self) -> SharedWorkspace:
        """Public read access to the team's shared workspace.

        In hub-and-spoke mode, workspace is used for output preservation only,
        not for cross-phase state sharing.
        """
        return self._workspace

    @property
    def handoff_transport(self):
        """Public read access to the team's handoff transport.

        In hub-and-spoke mode, handoff_transport is used for event broadcasting
        (team_formed, expert_step, expert_result, team_synthesis, team_dissolved),
        not for inter-agent communication.
        """
        return self._handoff_transport

    @property
    def team_channel(self) -> str:
        """Public read access to the team's communication channel."""
        return self._team_channel

    @property
    def pool(self) -> AgentPool | None:
        """Public read access to the team's AgentPool.

        Used by TeamOrchestrator to create independent ConfigDrivenAgent
        instances for context isolation in pipeline mode (KTD3).
        """
        return self._pool

    def get_expert(self, name: str) -> Expert | None:
        """Get an expert by name. Returns None if not found."""
        return self._experts.get(name)

    def set_status(self, status: TeamStatus) -> None:
        """Update the team's status."""
        self._status = status

    async def create_team(
        self,
        lead_config: ExpertConfig,
        member_configs: list[ExpertConfig] | None = None,
    ) -> None:
        """Create a team with a Lead Expert and optional members.

        In hub-and-spoke mode, the Lead Expert acts as the hub:
        - Receives the task and decomposes it into subtasks
        - Dispatches subtasks to member experts (spokes)
        - Synthesizes the final result
        """
        if not self._pool:
            raise RuntimeError("AgentPool not configured")

        # Create Lead Expert
        team_context = self._build_team_context(lead_config, member_configs or [])
        lead = await Expert.create(
            config=lead_config,
            pool=self._pool,
            handoff_transport=self._handoff_transport,
            workspace=self._workspace,
            team_context=team_context,
        )
        lead.team_id = self.team_id
        self._experts[lead_config.name] = lead
        self._lead_expert_name = lead_config.name

        # Create member Experts
        if member_configs:
            for config in member_configs:
                await self._add_expert_internal(config, team_context)

        # KTD6: 设置 PLANNING 状态（Lead Expert 即将分解任务为阶段）
        self._status = TeamStatus.PLANNING

    async def add_expert(self, config_or_template: ExpertConfig | str) -> Expert:
        """Add an Expert to the team dynamically.

        Args:
            config_or_template: ExpertConfig instance or template name to look up
        """
        if isinstance(config_or_template, str):
            template = self._template_registry.get(config_or_template)
            if template is None:
                raise ValueError(f"ExpertTemplate '{config_or_template}' not found")
            config = template.config
        else:
            config = config_or_template

        # Safely get lead config — _lead_expert_name may be stale
        lead_config: ExpertConfig | None = None
        if self._lead_expert_name and self._lead_expert_name in self._experts:
            lead_config = self._experts[self._lead_expert_name].config

        team_context = self._build_team_context(
            lead_config,
            [e.config for e in self.active_experts],
        )
        return await self._add_expert_internal(config, team_context)

    async def _add_expert_internal(self, config: ExpertConfig, team_context: str) -> Expert:
        """Internal method to add an Expert."""
        if not self._pool:
            raise RuntimeError("AgentPool not configured")

        expert = await Expert.create(
            config=config,
            pool=self._pool,
            handoff_transport=self._handoff_transport,
            workspace=self._workspace,
            team_context=team_context,
        )
        expert.team_id = self.team_id
        self._experts[config.name] = expert

        # Broadcast new expert joined
        await self._handoff_transport.send(
            self._team_channel,
            {
                "type": "expert_joined",
                "expert_name": config.name,
                "capabilities": expert.get_capabilities_summary(),
            },
        )

        return expert

    async def remove_expert(self, name: str) -> None:
        """Remove an Expert from the team."""
        expert = self._experts.get(name)
        if not expert:
            return

        # Cannot remove Lead Expert — must reassign first
        if name == self._lead_expert_name:
            active = [e for e in self.active_experts if e.config.name != name]
            if active:
                # Reassign lead to first active expert
                new_lead = active[0]
                self._lead_expert_name = new_lead.config.name
                new_lead.config.is_lead = True
            else:
                self._lead_expert_name = None

        await expert.destroy(self._pool)
        del self._experts[name]

        # Broadcast expert left
        await self._handoff_transport.send(
            self._team_channel,
            {
                "type": "expert_left",
                "expert_name": name,
            },
        )

    async def broadcast_user_message(self, content: str) -> None:
        """Broadcast a user intervention message to all active Experts.

        Also enqueues the message to the intervention queue so
        TeamOrchestrator can consume it at phase boundaries (U4).
        """
        message = {
            "type": "user_intervention",
            "content": content,
            "timestamp": time.time(),
        }
        await self._handoff_transport.send(self._team_channel, message)
        # U4: enqueue for orchestrator consumption (non-blocking; drop on full)
        try:
            self._interventions.put_nowait(content)
        except asyncio.QueueFull:
            logger.warning("Intervention queue full, dropping message")

    async def add_user_intervention(self, content: str) -> None:
        """Add a user intervention message for the orchestrator to consume.

        Broadcasts the message to the team channel and enqueues it.
        Used by WS/CLI handlers during team execution (U4).

        Args:
            content: User's intervention message (e.g. ``/debate <topic>``,
                ``/stop``, or plain text to append to Lead context)
        """
        await self.broadcast_user_message(content)

    def consume_user_interventions(self) -> list[str]:
        """Drain and return all pending user interventions.

        Called by TeamOrchestrator at phase boundaries (U4).

        Returns:
            List of intervention messages (oldest first). Empty if none.
        """
        interventions: list[str] = []
        while not self._interventions.empty():
            try:
                interventions.append(self._interventions.get_nowait())
            except asyncio.QueueEmpty:
                break
        return interventions

    async def get_shared_context(self) -> dict:
        """Get the team's shared context from SharedWorkspace.

        In hub-and-spoke mode, this returns preserved outputs only,
        not cross-phase state.
        """
        context = {}
        keys = await self._workspace.list_keys()
        for key in keys:
            if key.startswith(f"team:{self.team_id}"):
                data = await self._workspace.read(key)
                if data:
                    context[key] = data
        return context

    async def dissolve(self) -> None:
        """Dissolve the team. Temporary Experts are recycled, outputs preserved in SharedWorkspace."""
        # Cancel ongoing orchestrator task if any
        if self._orchestrator_task and not self._orchestrator_task.done():
            self._orchestrator_task.cancel()
            try:
                await self._orchestrator_task
            except asyncio.CancelledError:
                pass
            self._orchestrator_task = None

        for expert in self._experts.values():
            if expert.is_active and self._pool:
                await expert.destroy(self._pool)

        self._experts.clear()
        self._lead_expert_name = None
        self._status = TeamStatus.DISSOLVED

        # Close handoff transport
        self._handoff_transport.close()

    def _build_team_context(
        self,
        lead_config: ExpertConfig | None,
        member_configs: list[ExpertConfig],
    ) -> str:
        """Build team context string for injection into Expert system prompts.

        In hub-and-spoke mode, the context emphasizes the Lead Expert's role
        as the hub and member experts' role as spokes.
        """
        lines = ["You are part of an Expert Team (hub-and-spoke mode)."]

        if lead_config:
            lines.append(
                f"Lead Expert (hub): {lead_config.name} ({lead_config.persona}) — "
                f"decomposes tasks, dispatches subtasks, and synthesizes results."
            )

        for config in member_configs:
            if lead_config and config.name == lead_config.name:
                continue
            lines.append(
                f"Team Member (spoke): {config.name} ({config.persona}), "
                f"Skills: {', '.join(config.bound_skills)} — "
                f"executes assigned subtasks independently."
            )

        lines.append(
            "In hub-and-spoke mode: Lead Expert holds all state, "
            "subtasks are independent (depth=1, no further spawning), "
            "no inter-agent communication."
        )
        return "\n".join(lines)
