"""GoalPlanner — 目标分析与计划生成

用户给定自然语言目标后，自动生成结构化执行计划，包含任务拆解、
依赖关系、并行度识别。作为 Orchestrator._decompose_task() 的前置增强层。

执行流程：
1. 通过结构化目标分解（规则/模板）生成初始方案
2. 如果初始方案有效则跳过 LLM 调用
3. 否则将初始方案作为上下文注入 LLM prompt，LLM 细化调整
4. 识别能力缺口，请求人工介入
5. 通过 AskHumanTool 请求确认/修改
"""

from __future__ import annotations

import logging
import re
import uuid
from typing import Any

from agentkit.core.plan_schema import (
    ExecutionPlan,
    PlanStep,
    PlanStepStatus,
    SkillGap,
    SkillGapLevel,
)

logger = logging.getLogger(__name__)


class GoalPlanner:
    """目标分析与计划生成器

    将自然语言目标分解为结构化执行计划，包含任务拆解、
    依赖关系和并行度识别。

    使用方式：
        planner = GoalPlanner()
        plan = await planner.generate_plan(
            goal="调研 3 个竞品 SEO 策略并生成对比报告",
            context={},
            available_skills=["web_search", "seo_analyzer", "report_generator"],
        )
    """

    def __init__(self, llm_gateway: Any = None, max_parallel: int = 5):
        """
        Args:
            llm_gateway: LLM Gateway，用于细化计划（可选）
            max_parallel: 最大并行步骤数
        """
        self._llm_gateway = llm_gateway
        self._max_parallel = max_parallel

    async def generate_plan(
        self,
        goal: str,
        context: dict[str, Any] | None = None,
        available_skills: list[str] | None = None,
    ) -> ExecutionPlan:
        """生成结构化执行计划

        Args:
            goal: 自然语言目标
            context: 上下文信息（如已有数据、约束条件等）
            available_skills: 可用 Skill 列表

        Returns:
            ExecutionPlan: 结构化执行计划
        """
        context = context or {}
        available_skills = available_skills or []

        # 1. 通过规则/模板生成初始方案
        plan = self._rule_based_decompose(goal, context, available_skills)

        # 2. 识别能力缺口
        plan.skill_gaps = self._identify_skill_gaps(plan, available_skills)

        # 3. 如果有 LLM Gateway 且初始方案不够精确，让 LLM 细化
        if self._llm_gateway and self._should_refine_with_llm(plan):
            plan = await self._llm_refine_plan(goal, plan, context, available_skills)
            # 细化后重新识别能力缺口
            plan.skill_gaps = self._identify_skill_gaps(plan, available_skills)

        # 4. 构建并行组
        plan.parallel_groups = self._build_parallel_groups(plan.steps)

        return plan

    def _rule_based_decompose(
        self,
        goal: str,
        context: dict[str, Any],
        available_skills: list[str],
    ) -> ExecutionPlan:
        """基于规则/模板的目标分解

        使用启发式规则识别目标中的并列结构和顺序依赖，
        生成初始执行计划。
        """
        steps: list[PlanStep] = []

        # 识别并列结构：如"3 个竞品"、"3个方案"、"A、B、C"
        parallel_items = self._extract_parallel_items(goal)

        if parallel_items and len(parallel_items) > 1:
            # 有并列结构：每个并列项生成一个并行步骤 + 汇总步骤
            steps = self._decompose_parallel_goal(goal, parallel_items, available_skills)
        else:
            # 无明显并列结构：尝试识别顺序步骤
            sequential_parts = self._extract_sequential_parts(goal)
            if len(sequential_parts) > 1:
                steps = self._decompose_sequential_goal(goal, sequential_parts, available_skills)
            else:
                # 单步任务
                steps = self._decompose_simple_goal(goal, available_skills)

        return ExecutionPlan(
            goal=goal,
            steps=steps,
        )

    def _extract_parallel_items(self, goal: str) -> list[str]:
        """从目标中提取并列项

        识别模式：
        - "N 个 X"：如"3 个竞品"、"5 个方案"
        - "A、B、C"：顿号分隔的并列项
        - "A, B, C"：逗号分隔的并列项
        """
        items: list[str] = []

        # 模式1："N 个 X" — 识别数量+类别
        count_match = re.search(r"(\d+)\s*个\s*(.+?)(?:的|和|并|以及|，|,|$)", goal)
        if count_match:
            count = int(count_match.group(1))
            category = count_match.group(2).strip()
            # 生成 N 个并列项
            for i in range(1, count + 1):
                items.append(f"{category} {i}")
            return items

        # 模式2：顿号分隔 — "竞品A、竞品B、竞品C"
        if "、" in goal:
            # 提取顿号分隔的片段
            parts = re.split(r"[、]", goal)
            # 过滤掉太短的片段（可能是标点噪声）
            meaningful = [p.strip() for p in parts if len(p.strip()) > 1]
            if len(meaningful) >= 2:
                items = meaningful
                return items

        # 模式3：英文逗号分隔 — "A, B, C"
        if "," in goal:
            parts = goal.split(",")
            meaningful = [p.strip() for p in parts if len(p.strip()) > 1]
            if len(meaningful) >= 2:
                items = meaningful
                return items

        return items

    def _extract_sequential_parts(self, goal: str) -> list[str]:
        """从目标中提取顺序步骤

        识别模式：
        - "并"：如"调研并生成报告"
        - "然后"/"接着"/"再"：顺序连接词
        - "→"/"->"：箭头分隔
        """
        parts: list[str] = []

        # 模式1：箭头分隔
        if "→" in goal or "->" in goal:
            separator = "→" if "→" in goal else "->"
            parts = [p.strip() for p in goal.split(separator) if p.strip()]
            return parts

        # 模式2：顺序连接词
        sequential_patterns = [
            r"(.+?)然后(.+)",
            r"(.+?)接着(.+)",
            r"(.+?)之后再(.+)",
        ]
        for pattern in sequential_patterns:
            match = re.search(pattern, goal)
            if match:
                parts = [g.strip() for g in match.groups() if g.strip()]
                return parts

        # 模式3："并" 连接 — 如"调研并生成报告"
        if "并" in goal:
            match = re.search(r"(.+?)并(.+)", goal)
            if match:
                parts = [g.strip() for g in match.groups() if g.strip()]
                return parts

        return parts

    def _decompose_parallel_goal(
        self,
        goal: str,
        parallel_items: list[str],
        available_skills: list[str],
    ) -> list[PlanStep]:
        """分解包含并列结构的目标

        生成 N 个并行步骤 + 1 个汇总步骤。
        """
        steps: list[PlanStep] = []
        parallel_step_ids: list[str] = []

        # 为每个并列项生成一个并行步骤
        for i, item in enumerate(parallel_items):
            step_id = f"step-{i}"
            required_skills = self._infer_required_skills(item, available_skills)
            steps.append(PlanStep(
                step_id=step_id,
                name=f"处理: {item}",
                description=f"对「{item}」执行相关操作",
                dependencies=[],
                parallel_group=0,
                required_skills=required_skills,
            ))
            parallel_step_ids.append(step_id)

        # 汇总步骤：依赖所有并行步骤
        summary_skills = self._infer_required_skills("汇总 生成 报告", available_skills)
        steps.append(PlanStep(
            step_id=f"step-{len(parallel_items)}",
            name="汇总结果",
            description="汇总所有并行步骤的结果，生成最终输出",
            dependencies=parallel_step_ids,
            parallel_group=1,
            required_skills=summary_skills,
        ))

        return steps

    def _decompose_sequential_goal(
        self,
        goal: str,
        sequential_parts: list[str],
        available_skills: list[str],
    ) -> list[PlanStep]:
        """分解包含顺序步骤的目标"""
        steps: list[PlanStep] = []

        for i, part in enumerate(sequential_parts):
            step_id = f"step-{i}"
            dependencies = [f"step-{i - 1}"] if i > 0 else []
            required_skills = self._infer_required_skills(part, available_skills)
            steps.append(PlanStep(
                step_id=step_id,
                name=part[:50],  # 截取前 50 字符作为名称
                description=part,
                dependencies=dependencies,
                parallel_group=i,
                required_skills=required_skills,
            ))

        return steps

    def _decompose_simple_goal(
        self,
        goal: str,
        available_skills: list[str],
    ) -> list[PlanStep]:
        """分解简单目标为单步计划"""
        required_skills = self._infer_required_skills(goal, available_skills)
        return [
            PlanStep(
                step_id="step-0",
                name=goal[:50],
                description=goal,
                dependencies=[],
                parallel_group=0,
                required_skills=required_skills,
            )
        ]

    def _infer_required_skills(self, text: str, available_skills: list[str]) -> list[str]:
        """根据文本推断所需的 Skill

        基于关键词匹配，将文本中的意图映射到可用 Skill。
        """
        skill_keywords: dict[str, list[str]] = {
            "web_search": ["搜索", "查询", "查找", "调研", "search", "find", "lookup"],
            "seo_analyzer": ["seo", "搜索引擎优化", "关键词", "排名"],
            "report_generator": ["报告", "汇总", "总结", "生成", "对比", "report", "summary"],
            "data_analyzer": ["分析", "统计", "数据", "analyze", "data"],
            "document_writer": ["写", "撰写", "文档", "write", "document"],
            "code_generator": ["代码", "编程", "开发", "code", "develop"],
        }

        text_lower = text.lower()
        matched: list[str] = []

        for skill, keywords in skill_keywords.items():
            if skill not in available_skills:
                continue
            if any(kw in text_lower for kw in keywords):
                matched.append(skill)

        return matched

    def _identify_skill_gaps(
        self, plan: ExecutionPlan, available_skills: list[str]
    ) -> list[SkillGap]:
        """识别能力缺口

        检查每个步骤所需的 Skill 是否可用，标注缺口。
        """
        gaps: list[SkillGap] = []
        available_set = set(available_skills)

        for step in plan.steps:
            for skill in step.required_skills:
                if skill not in available_set:
                    gaps.append(SkillGap(
                        step_name=step.name,
                        required_skill=skill,
                        level=SkillGapLevel.HIGH,
                        suggestion=f"请安装或注册 '{skill}' Skill，或手动完成该步骤",
                    ))

            # 如果步骤没有匹配到任何 Skill，标注缺口
            if not step.required_skills:
                if not available_skills:
                    # 无可用 Skill 时标注为 HIGH
                    gaps.append(SkillGap(
                        step_name=step.name,
                        required_skill="(无可用 Skill)",
                        level=SkillGapLevel.HIGH,
                        suggestion="当前无可用 Skill，请注册所需 Skill 或手动完成该步骤",
                    ))
                else:
                    # 有 Skill 但未匹配到时标注为 MEDIUM
                    gaps.append(SkillGap(
                        step_name=step.name,
                        required_skill="(未匹配)",
                        level=SkillGapLevel.MEDIUM,
                        suggestion=f"无法自动匹配 Skill，可用 Skill: {', '.join(available_skills[:5])}",
                    ))

        return gaps

    def _should_refine_with_llm(self, plan: ExecutionPlan) -> bool:
        """判断是否需要 LLM 细化

        当初始方案步骤描述过于简单、能力缺口较多、或所有步骤
        都没有匹配到 Skill 时，需要 LLM 细化。
        """
        # 如果所有步骤都没有匹配到任何 Skill，让 LLM 重新评估
        if plan.steps and all(not s.required_skills for s in plan.steps):
            return True

        # 如果有较多能力缺口，让 LLM 重新评估
        if len(plan.skill_gaps) > len(plan.steps):
            return True

        return False

    async def _llm_refine_plan(
        self,
        goal: str,
        initial_plan: ExecutionPlan,
        context: dict[str, Any],
        available_skills: list[str],
    ) -> ExecutionPlan:
        """使用 LLM 细化执行计划

        将初始方案作为上下文注入 LLM prompt，让 LLM 细化调整。
        """
        import json

        # 构建初始方案摘要
        initial_summary = json.dumps(
            [s.to_dict() for s in initial_plan.steps],
            ensure_ascii=False,
            indent=2,
        )

        skills_str = ", ".join(available_skills) if available_skills else "无"

        prompt = (
            f"Refine the following execution plan for the given goal.\n\n"
            f"Goal: {goal}\n\n"
            f"Initial Plan (generated by rules):\n{initial_summary}\n\n"
            f"Available Skills: {skills_str}\n\n"
            f"Context: {json.dumps(context, ensure_ascii=False) if context else 'None'}\n\n"
            'Respond ONLY with a JSON array of steps: '
            '[{"name": "...", "description": "...", "dependencies": [], '
            '"required_skills": [...]}]\n'
            "The dependencies field lists step indices (0-based) that must complete first.\n"
            "Each step should have a clear, specific description (at least 20 characters).\n"
            "Do not include any other text."
        )

        try:
            response = await self._llm_gateway.chat(
                messages=[{"role": "user", "content": prompt}],
                model="default",
            )

            step_defs = json.loads(response.content)
            if not isinstance(step_defs, list) or not step_defs:
                return initial_plan

            steps: list[PlanStep] = []
            for i, defn in enumerate(step_defs):
                depends_on = [f"step-{j}" for j in defn.get("dependencies", [])]
                steps.append(PlanStep(
                    step_id=f"step-{i}",
                    name=defn.get("name", f"Step {i}"),
                    description=defn.get("description", ""),
                    dependencies=depends_on,
                    parallel_group=0,  # 后续由 _build_parallel_groups 重新计算
                    required_skills=defn.get("required_skills", []),
                ))

            return ExecutionPlan(
                goal=goal,
                steps=steps,
                metadata={"refined_by_llm": True},
            )

        except Exception as e:
            logger.warning(f"LLM plan refinement failed, using initial plan: {e}")
            return initial_plan

    def _build_parallel_groups(self, steps: list[PlanStep]) -> list[list[str]]:
        """构建并行执行组

        基于依赖关系拓扑排序，无依赖的步骤分到同一组并行执行。
        复用 Orchestrator._build_parallel_groups() 的拓扑排序逻辑。
        """
        step_map = {s.step_id: s for s in steps}
        completed: set[str] = set()
        groups: list[list[str]] = []
        remaining = set(s.step_id for s in steps)

        while remaining:
            # 找到所有依赖已满足的步骤
            ready = []
            for sid in remaining:
                step = step_map[sid]
                if all(dep in completed for dep in step.dependencies):
                    ready.append(sid)

            if not ready:
                # 循环依赖 — 将剩余步骤放入一组
                groups.append(list(remaining))
                break

            # 限制组大小
            group = ready[: self._max_parallel]
            groups.append(group)
            for sid in group:
                completed.add(sid)
                remaining.discard(sid)

        # 更新步骤的 parallel_group 字段
        for group_idx, group in enumerate(groups):
            for sid in group:
                step = step_map.get(sid)
                if step:
                    step.parallel_group = group_idx

        return groups

    def update_plan_from_feedback(
        self,
        plan: ExecutionPlan,
        modifications: dict[str, Any],
    ) -> ExecutionPlan:
        """根据用户反馈更新计划

        Args:
            plan: 原始执行计划
            modifications: 修改内容，可包含：
                - add_steps: 新增步骤列表
                - remove_steps: 要移除的步骤 ID 列表
                - update_steps: 要更新的步骤 {step_id: {field: value}}
                - reorder: 是否重新排序

        Returns:
            更新后的 ExecutionPlan
        """
        steps = list(plan.steps)

        # 移除步骤
        remove_ids = set(modifications.get("remove_steps", []))
        if remove_ids:
            steps = [s for s in steps if s.step_id not in remove_ids]
            # 清理依赖引用
            for step in steps:
                step.dependencies = [d for d in step.dependencies if d not in remove_ids]

        # 更新步骤
        update_map: dict[str, dict] = modifications.get("update_steps", {})
        for step in steps:
            if step.step_id in update_map:
                updates = update_map[step.step_id]
                for field_name, value in updates.items():
                    if hasattr(step, field_name):
                        setattr(step, field_name, value)

        # 新增步骤
        add_steps = modifications.get("add_steps", [])
        for new_step_def in add_steps:
            step_id = new_step_def.get("step_id", f"step-{len(steps)}")
            # 确保唯一性
            existing_ids = {s.step_id for s in steps}
            while step_id in existing_ids:
                step_id = f"step-{uuid.uuid4().hex[:4]}"

            steps.append(PlanStep(
                step_id=step_id,
                name=new_step_def.get("name", "New Step"),
                description=new_step_def.get("description", ""),
                dependencies=new_step_def.get("dependencies", []),
                required_skills=new_step_def.get("required_skills", []),
            ))

        # 重新构建并行组
        parallel_groups = self._build_parallel_groups(steps)

        return ExecutionPlan(
            plan_id=plan.plan_id,
            goal=plan.goal,
            steps=steps,
            parallel_groups=parallel_groups,
            skill_gaps=plan.skill_gaps,  # 保留原有缺口信息
            confirmed=False,  # 修改后需要重新确认
            metadata=plan.metadata,
        )

    def validate_plan(self, plan: ExecutionPlan) -> list[str]:
        """验证执行计划的合法性

        Returns:
            错误信息列表，空列表表示验证通过
        """
        errors: list[str] = []
        step_ids = {s.step_id for s in plan.steps}

        # 检查依赖引用是否存在
        for step in plan.steps:
            for dep in step.dependencies:
                if dep not in step_ids:
                    errors.append(f"步骤 '{step.step_id}' 依赖不存在的步骤 '{dep}'")

        # 检查循环依赖
        visited: set[str] = set()
        in_stack: set[str] = set()

        def has_cycle(sid: str) -> bool:
            if sid in in_stack:
                return True
            if sid in visited:
                return False
            visited.add(sid)
            in_stack.add(sid)
            step = plan.get_step(sid)
            if step:
                for dep in step.dependencies:
                    if has_cycle(dep):
                        return True
            in_stack.discard(sid)
            return False

        for step in plan.steps:
            if has_cycle(step.step_id):
                errors.append(f"检测到循环依赖，涉及步骤 '{step.step_id}'")
                break

        # 检查并行组与步骤的一致性
        grouped_ids: set[str] = set()
        for group in plan.parallel_groups:
            for sid in group:
                if sid not in step_ids:
                    errors.append(f"并行组包含不存在的步骤 '{sid}'")
                if sid in grouped_ids:
                    errors.append(f"步骤 '{sid}' 出现在多个并行组中")
                grouped_ids.add(sid)

        ungrouped = step_ids - grouped_ids
        if ungrouped:
            errors.append(f"步骤未分配到并行组: {', '.join(ungrouped)}")

        return errors
