"""PhaseExecutorMixin — 阶段执行 + 隔离 agent + 协作通知。

# TYPE_CHECKING: 由 TeamOrchestrator 组合，访问 self 共享状态
"""

from __future__ import annotations

import asyncio
import copy
import logging
from datetime import datetime, timezone
from typing import TYPE_CHECKING

from agentkit.core.config_driven import ConfigDrivenAgent
from agentkit.core.protocol import TaskMessage, TaskResult, TaskStatus

from .expert import Expert
from .plan import PhaseStatus, PhaseType, PlanPhase, TeamPlan

if TYPE_CHECKING:
    from .team import ExpertTeam

logger = logging.getLogger(__name__)


class PhaseExecutorMixin:
    """Mixin: 阶段执行 + 隔离 agent + 状态卸载 + 协作通知。由 TeamOrchestrator 组合。"""

    # Shared state provided by TeamOrchestrator (annotations only, no runtime effect)
    _team: ExpertTeam
    _temp_agents: dict[str, str]
    _phase_semaphore: asyncio.Semaphore
    MAX_RETRIES: int
    MAX_REWORKS: int
    MAX_RISK_FLAGS: int

    # U4: State offloading helpers — keep memory lean for long-horizon runs.
    _OFFLOAD_SUMMARY_LIMIT = 500

    def _offload_result(self, content: str, ref_key: str) -> dict[str, object]:
        """Create an offloaded result: summary in memory, full content in workspace."""
        if not isinstance(content, str):
            content = str(content) if content is not None else ""
        summary = (
            content[: self._OFFLOAD_SUMMARY_LIMIT] + "..."
            if len(content) > self._OFFLOAD_SUMMARY_LIMIT
            else content
        )
        return {"content": summary, "_ref_key": ref_key, "_offloaded": True}

    async def _read_dependency_output(self, dep_phase: PlanPhase) -> str:
        """Read a dependency phase's output, resolving offloaded content from workspace."""
        if not dep_phase.result:
            return ""
        content = dep_phase.result.get("content", str(dep_phase.result))
        if dep_phase.result.get("_offloaded"):
            ref_key = dep_phase.result.get("_ref_key", "")
            if ref_key:
                try:
                    full_data = await self._team.workspace.read(ref_key)
                    if full_data:
                        return full_data.get("value", content)
                except (asyncio.TimeoutError, ConnectionError, KeyError, AttributeError) as e:
                    logger.warning(f"Failed to read offloaded output '{ref_key}': {e}")
        return content

    async def _execute_phase(self, phase: PlanPhase, plan: TeamPlan) -> dict[str, object]:
        """Execute a single phase, dispatching by phase_type."""
        if phase.phase_type == PhaseType.DEBATE:
            return await self._execute_debate_phase(phase, plan)
        return await self._execute_execution_phase(phase, plan)

    async def _execute_execution_phase(self, phase: PlanPhase, plan: TeamPlan) -> dict[str, object]:
        """Execute a standard EXECUTION phase. Split into 3 sub-methods (U2, KTD3 isolation)."""
        expert, agent, lead = await self._prepare_phase_context(phase, plan)
        last_error: str | None = None
        result: dict[str, object] | None = None

        try:
            # U3: 返工循环 — 最多 MAX_REWORKS + 1 次（1 次初始 + MAX_REWORKS 次返工）
            for _rework_attempt in range(self.MAX_REWORKS + 1):
                result, last_error, passed, feedback, degraded = await self._run_agent_steps(
                    expert, agent, lead, phase, plan
                )
                done = await self._finalize_phase(
                    expert, lead, phase, plan, result, passed, feedback, degraded
                )
                if done:
                    return result
        finally:
            await self._cleanup_isolated_agent(phase)

        # Should not reach here
        phase.status = PhaseStatus.FAILED
        await self._broadcast_event(
            "phase_failed",
            {
                "phase_id": phase.id,
                "phase_name": phase.name,
                "error": last_error or "unknown error",
            },
        )
        raise RuntimeError(f"Phase {phase.id} ({phase.name}) failed: {last_error}")

    async def _prepare_phase_context(
        self, phase: PlanPhase, plan: TeamPlan
    ) -> tuple[Expert, ConfigDrivenAgent, Expert]:
        """Resolve expert, set RUNNING, emit phase_started, get isolated agent."""
        expert = self._team.get_expert(phase.assigned_expert)
        if not expert or not expert.is_active:
            expert = self._team.lead_expert
            if not expert or not expert.is_active:
                active = self._team.active_experts
                if not active:
                    raise RuntimeError(
                        f"Expert '{phase.assigned_expert}' not available and no active fallback"
                    )
                expert = active[0]
            logger.warning(
                f"Expert '{phase.assigned_expert}' not available, "
                f"falling back to '{expert.config.name}'"
            )
            phase.assigned_expert = expert.config.name

        phase.status = PhaseStatus.RUNNING
        await self._broadcast_event("phase_started", {
            "phase_id": phase.id, "phase_name": phase.name,
            "assigned_expert": phase.assigned_expert, "depends_on": list(phase.depends_on),
        })
        agent = await self._get_isolated_agent(expert, phase)
        lead = self._team.lead_expert or expert
        return expert, agent, lead

    def _build_task_message(
        self,
        expert: Expert,
        phase: PlanPhase,
        dependency_outputs: dict[str, object],
        collaboration_outputs: dict[str, str],
    ) -> TaskMessage:
        """Build TaskMessage for execution with context isolation."""
        input_data: dict[str, object] = {
            "task": phase.task_description,
            "team_id": self._team.team_id,
            "phase_id": phase.id,
            "phase_name": phase.name,
            "is_phase": True,
            "dependency_outputs": dependency_outputs,
        }
        if dependency_outputs:
            input_data["context"] = "前置阶段输出:\n" + "\n---\n".join(
                f"[{name}]:\n"
                f"{output[:500] if isinstance(output, str) else str(output)[:500]}"
                for name, output in dependency_outputs.items()
            )
        if collaboration_outputs:
            collab_context = "协作专家输出:\n" + "\n---\n".join(
                f"[{exp}]: {output[:500] if isinstance(output, str) else str(output)[:500]}"
                for exp, output in collaboration_outputs.items()
            )
            if "context" in input_data:
                input_data["context"] += "\n\n" + collab_context
            else:
                input_data["context"] = collab_context
            input_data["collaboration_outputs"] = collaboration_outputs
        return TaskMessage(
            task_id=phase.id,
            agent_name=expert.config.name,
            task_type="team_phase",
            priority=0,
            input_data=input_data,
            callback_url=None,
            created_at=datetime.now(timezone.utc),
        )

    async def _run_agent_steps(
        self,
        expert: Expert,
        agent: ConfigDrivenAgent,
        lead: Expert,
        phase: PlanPhase,
        plan: TeamPlan,
    ) -> tuple[dict[str, object], str | None, bool, str, bool]:
        """Run one rework iteration: read deps, build input, execute, review. Returns
        (result, last_error, passed, feedback, degraded). Raises RuntimeError on retry
        exhaustion."""
        # 每次迭代重新读取依赖输出（前置阶段可能在返工期间完成）
        dependency_outputs: dict[str, object] = {}
        for dep_id in phase.depends_on:
            dep_phase = plan.get_phase(dep_id)
            if dep_phase and dep_phase.status == PhaseStatus.COMPLETED and dep_phase.result:
                dependency_outputs[dep_phase.name] = await self._read_dependency_output(dep_phase)

        # 按协作契约读取相关专家的输出（可见性 — 打破上下文隔离，但限定在契约范围内）
        collaboration_outputs: dict[str, str] = {}
        for contract in phase.collaboration_contracts:
            if contract.from_expert and contract.status in ("delivered", "received"):
                for prev_phase in plan.phases:
                    if (
                        prev_phase.assigned_expert == contract.from_expert
                        and prev_phase.status == PhaseStatus.COMPLETED
                        and prev_phase.result
                    ):
                        collaboration_outputs[
                            contract.from_expert
                        ] = await self._read_dependency_output(prev_phase)
                        break

        await self._broadcast_event("expert_step", {
            "expert_id": expert.config.name, "expert_name": expert.config.name,
            "expert_color": expert.config.color, "content": phase.task_description,
            "step": phase.id, "phase_id": phase.id, "phase_name": phase.name,
        })

        task_msg = self._build_task_message(expert, phase, dependency_outputs, collaboration_outputs)

        # 执行专家任务（带重试，MAX_RETRIES 处理瞬时失败）
        last_error: str | None = None
        result: dict[str, object] | None = None
        for attempt in range(self.MAX_RETRIES + 1):
            try:
                task_result: TaskResult = await agent.execute(task_msg)
                if task_result.status != TaskStatus.COMPLETED.value:
                    last_error = task_result.error_message or "unknown error"
                    if attempt < self.MAX_RETRIES:
                        logger.info(f"Retrying phase {phase.id} (attempt {attempt + 1})")
                        continue
                    raise RuntimeError(f"Agent execution failed: {last_error}")
                result = task_result.output_data or {"content": ""}
                break
            except asyncio.CancelledError:
                # CancelledError 必须传播，不被重试逻辑吞掉
                raise
            except (RuntimeError, asyncio.TimeoutError, ConnectionError) as e:
                # agent.execute() 内部已捕获所有异常并返回 TaskResult，
                # 此处仅捕获显式抛出的 RuntimeError + 罕见的基础设施异常
                last_error = str(e)
                if attempt < self.MAX_RETRIES:
                    logger.info(f"Retrying phase {phase.id} (attempt {attempt + 1})")
                    continue
                raise

        await self._broadcast_event("expert_result", {
            "expert_id": expert.config.name, "expert_name": expert.config.name,
            "expert_color": expert.config.color, "content": result.get("content", str(result)),
            "phase_id": phase.id, "rework_attempt": phase.rework_count,
        })

        # U4: 解析专家输出中的风险标记，发出 risk_flagged 事件
        content = result.get("content", str(result))
        risk_flags = self._parse_risk_flags(content)
        for risk_desc in risk_flags[: self.MAX_RISK_FLAGS]:
            await self._broadcast_event("risk_flagged", {
            "expert": phase.assigned_expert, "expert_name": phase.assigned_expert,
            "risk_description": risk_desc, "phase_id": phase.id, "phase_name": phase.name,
        })

        # U3: Lead 验收阶段输出 — ReviewResult 结构化结果（含 degraded 标记）
        review = await self._review_phase_output(lead, phase, result)
        return result, last_error, review.passed, review.feedback, review.degraded

    async def _finalize_phase(
        self,
        expert: Expert,
        lead: Expert,
        phase: PlanPhase,
        plan: TeamPlan,
        result: dict[str, object],
        passed: bool,
        feedback: str,
        degraded: bool = False,
    ) -> bool:
        """Handle review outcome: write workspace + emit completed, or rework/fail. Returns
        True if done (COMPLETED), False if rework continues. Raises on rework limit.

        Args:
            degraded: True 表示验收走了降级路径（LLM 不可用/超时/异常时自动通过），
                广播到 ``review_result`` 事件 payload 让前端/运维可编程判断。
        """
        if passed:
            phase.status = PhaseStatus.COMPLETED
            # P2: SharedWorkspace 写入移到验收通过后 — 避免持久化被拒输出
            output_key = f"{plan.id}/phase/{phase.id}/output"
            full_content = result.get("content", str(result))
            await self._team.workspace.write(output_key, full_content, expert.config.name)
            phase.result = self._offload_result(full_content, output_key)
            await self._broadcast_event("review_result", {
                "phase_id": phase.id, "phase_name": phase.name, "passed": True,
                "feedback": feedback, "expert": phase.assigned_expert,
                "degraded": degraded,
            })
            if phase.collaboration_contracts:
                await self._notify_collaborators(phase, plan)
            result_summary = result.get("content", str(result))
            if isinstance(result_summary, str) and len(result_summary) > 200:
                result_summary = result_summary[:200] + "..."
            await self._broadcast_event("phase_completed", {
                "phase_id": phase.id, "phase_name": phase.name,
                "result_summary": result_summary,
            })
            return True

        # 验收不合格 — 返工或标记失败（degraded 路径不应走到这里，但保持字段一致）
        phase.rework_count += 1
        phase.review_feedback = feedback

        if phase.rework_count > self.MAX_REWORKS:
            phase.status = PhaseStatus.FAILED
            await self._broadcast_event(
                "review_result",
                {
                    "phase_id": phase.id,
                    "phase_name": phase.name,
                    "passed": False,
                    "feedback": feedback,
                    "expert": phase.assigned_expert,
                    "rework_count": phase.rework_count,
                    "final_status": "failed",
                    "degraded": degraded,
                },
            )
            await self._broadcast_event(
                "phase_failed",
                {
                    "phase_id": phase.id,
                    "phase_name": phase.name,
                    "error": f"Review failed after " f"{phase.rework_count} reworks: {feedback}",
                },
            )
            raise RuntimeError(
                f"Phase {phase.id} failed after {phase.rework_count} reworks: {feedback}"
            )

        # 准备返工，继续循环
        await self._broadcast_event(
            "review_result",
            {
                "phase_id": phase.id,
                "phase_name": phase.name,
                "passed": False,
                "feedback": feedback,
                "expert": phase.assigned_expert,
                "rework_count": phase.rework_count,
                "final_status": "rework",
                "degraded": degraded,
            },
        )
        feedback_truncated = feedback[:500] if feedback else ""
        phase.task_description += f"\n\n[返工要求]: {feedback_truncated}"
        return False

    async def _notify_collaborators(self, phase: PlanPhase, plan: TeamPlan) -> None:
        """阶段验收通过后，按协作契约通知相关专家，并同步契约状态为 delivered/received。"""
        for contract in phase.collaboration_contracts:
            if not contract.to_expert or contract.status == "delivered":
                continue
            to_expert = self._team.get_expert(contract.to_expert)
            expert_color = to_expert.config.color if to_expert else "#888888"
            await self._broadcast_event(
                "collaboration_notice",
                {
                    "from_expert": phase.assigned_expert,
                    "to_expert": contract.to_expert,
                    "content_description": contract.content_description,
                    "phase_id": phase.id,
                    "phase_name": phase.name,
                    "output_key": f"{plan.id}/phase/{phase.id}/output",
                    "expert_color": expert_color,
                },
            )
            contract.status = "delivered"
            # P0: 同步更新接收方阶段中对应的契约状态为 received
            for recv_phase in plan.phases:
                if recv_phase.assigned_expert != contract.to_expert:
                    continue
                for recv_contract in recv_phase.collaboration_contracts:
                    if (
                        recv_contract.from_expert == phase.assigned_expert
                        and recv_contract.status == "pending"
                    ):
                        recv_contract.status = "received"

    async def _get_isolated_agent(self, expert: Expert, phase: PlanPhase) -> ConfigDrivenAgent:
        """Get an isolated ConfigDrivenAgent instance for the phase (KTD3 context isolation)."""
        pool = self._team.pool
        if pool is None:
            return expert.agent
        temp_config = copy.deepcopy(expert.config)
        temp_config.name = f"{expert.config.name}__phase_{phase.id[:8]}"
        try:
            agent = await pool.create_agent(temp_config)
            self._temp_agents[phase.id] = temp_config.name
            return agent
        except (ValueError, KeyError, RuntimeError, TypeError) as e:
            # pool.create_agent 失败：config 校验/工具注册/依赖缺失等
            logger.warning(
                f"Failed to create isolated agent for phase {phase.id}, "
                f"using expert's existing agent: {e}"
            )
            return expert.agent

    async def _cleanup_isolated_agent(self, phase: PlanPhase) -> None:
        """Clean up the temporary isolated agent if one was created."""
        pool = self._team.pool
        if pool is None:
            return
        temp_name = self._temp_agents.pop(phase.id, None)
        if temp_name:
            try:
                await pool.remove_agent(temp_name)
            except asyncio.CancelledError:
                raise
            except (KeyError, RuntimeError) as e:
                logger.warning(f"Failed to clean up isolated agent '{temp_name}': {e}")
