"""Shared skill routing logic for GUI and CLI chat.

Extracts the duplicated skill routing, @skill: prefix parsing,
and prompt assembly into a single module used by both chat routes.
"""

from __future__ import annotations

import enum
import logging
import re
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)

# Strict validation: only lowercase alphanumeric, hyphens, underscores
_SKILL_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")


class ExecutionMode(enum.Enum):
    """How the downstream should execute this routing result.

    This is the single source of truth for execution path selection.
    The transport layer (portal.py, chat.py) should branch on this
    field instead of string-matching match_method.
    """

    DIRECT_CHAT = "direct_chat"  # Zero-cost: direct LLM call, no ReAct loop
    REACT = "react"  # Default agent ReAct loop with default tools
    SKILL_REACT = "skill_react"  # Skill-matched ReAct with skill tools + prompt
    REWOO = "rewoo"  # Plan-without-observation mode
    REFLEXION = "reflexion"  # Reflection-driven mode
    PLAN_EXEC = "plan_exec"  # Plan-then-execute mode
    TEAM_COLLAB = "team_collab"  # Expert Team collaborative mode


# Mapping from skill config execution_mode string to ExecutionMode enum
_SKILL_EXECUTION_MODE_MAP: dict[str, ExecutionMode] = {
    "direct": ExecutionMode.DIRECT_CHAT,
    "react": ExecutionMode.SKILL_REACT,
    "rewoo": ExecutionMode.REWOO,
    "reflexion": ExecutionMode.REFLEXION,
    "plan_exec": ExecutionMode.PLAN_EXEC,
    "custom": ExecutionMode.SKILL_REACT,
    "llm_generate": ExecutionMode.SKILL_REACT,
    "tool_call": ExecutionMode.SKILL_REACT,
}


def _resolve_execution_mode(skill_config: object) -> ExecutionMode:
    """Resolve ExecutionMode from skill config's execution_mode field."""
    mode_str = getattr(skill_config, "execution_mode", "react") or "react"
    return _SKILL_EXECUTION_MODE_MAP.get(mode_str, ExecutionMode.SKILL_REACT)


def validate_skill_name(name: str) -> str:
    """Validate and normalize a skill name. Raises ValueError on invalid input."""
    normalized = name.strip().lower()
    if not _SKILL_NAME_RE.match(normalized):
        raise ValueError(f"Invalid skill name '{name}': must match [a-z0-9][a-z0-9_-]{{0,63}}")
    return normalized


@dataclass
class SkillRoutingResult:
    """Result of skill routing for a user message."""

    skill_name: str | None = None
    skill_config: object | None = None
    skill_tools: list[object] = field(default_factory=list)
    clean_content: str = ""
    system_prompt: str | None = None
    tools: list[object] = field(default_factory=list)
    model: str = "default"
    agent_name: str | None = None
    matched: bool = False
    match_method: str | None = None
    match_confidence: float = 0.0
    transparency_level: str = "SILENT"
    execution_trace: list[dict] = field(default_factory=list)
    complexity: float = 0.0
    execution_mode: ExecutionMode = ExecutionMode.DIRECT_CHAT


def parse_skill_prefix(content: str) -> tuple[str | None, str]:
    """Parse @skill:name prefix from user message.

    Returns (skill_name_or_None, clean_content).
    """
    if not content.startswith("@skill:"):
        return None, content

    parts = content.split(" ", 1)
    skill_ref = parts[0][7:]  # strip "@skill:"
    explicit_skill = skill_ref.strip()
    clean = parts[1].strip() if len(parts) > 1 else content[7 + len(skill_ref) :].strip()
    return explicit_skill, clean


_PROMPT_KEYS = ("identity", "context", "instructions", "constraints", "output_format")


def format_preconditions_block(preconditions: list[str], header_level: int = 2) -> str:
    """格式化激活前置条件文本块。"""
    header = "#" * header_level
    lines = [f"{header} Activation Preconditions", "Before executing this skill, verify:"]
    lines.extend(f"- {p}" for p in preconditions)
    lines.append(
        "If any precondition is not met, refuse to execute or ask the user for clarification."
    )
    return "\n".join(lines)


def collect_prompt_parts(config: object, with_headers: bool = False) -> list[str]:
    """从 skill config 的 prompt 字典中收集各部分文本。"""
    prompt = getattr(config, "prompt", None) or {}
    parts: list[str] = []
    for key in _PROMPT_KEYS:
        val = prompt.get(key)
        if val:
            parts.append(f"### {key.title()}\n{val}" if with_headers else val)
    return parts


def build_skill_system_prompt(skill_config) -> str | None:
    """Build system prompt from skill config's prompt section.

    v7: 若 skill_config.preconditions 非空，在基础 prompt 后追加
    ## Activation Preconditions 段落（软检查，见 KTD1）。

    U5: 渐进式技能加载 — 当 disclosure_level == 0 时，只注入 skill 名称 + 描述
    （概要模式）。LLM 可调用 skill_detail 工具按需加载完整 instructions。
    disclosure_level >= 1（默认）时行为不变（全量加载），向后兼容。
    """
    if not skill_config:
        return None

    # U5: Level 0 — 概要模式，只注入 name + description
    disclosure_level = getattr(skill_config, "disclosure_level", 1)
    if disclosure_level == 0:
        name = getattr(skill_config, "name", "unknown")
        description = getattr(skill_config, "description", "")
        summary = f"## Skill: {name}\n{description}" if description else f"## Skill: {name}"
        # 安全守卫：前置条件即使在概要模式下也必须注入
        preconditions = getattr(skill_config, "preconditions", None)
        if preconditions:
            return f"{summary}\n\n" + format_preconditions_block(preconditions)
        # 提示 LLM 可通过 skill_detail 工具加载完整 instructions
        return (
            f"{summary}\n\n"
            "*(Call the skill_detail tool with this skill name to load full instructions)*"
        )

    # Level 1+: 全量加载（现有行为）
    if not skill_config.prompt:
        return None
    base = "\n\n".join(collect_prompt_parts(skill_config)) or None

    # v7: 注入激活前置条件（软检查）
    preconditions = getattr(skill_config, "preconditions", None)
    if preconditions:
        preconditions_block = format_preconditions_block(preconditions)
        return f"{base}\n\n{preconditions_block}" if base else preconditions_block
    return base


async def resolve_skill_routing(
    content: str,
    skill_registry: object,
    default_tools: list[object],
    default_system_prompt: str | None,
    default_model: str = "default",
    default_agent_name: str = "default",
    agent_tool_registry: object | None = None,
    session_id: str = "",
) -> SkillRoutingResult:
    """Resolve skill routing for a user message.

    This is the shared entry point used by CLI chat.
    Uses @skill: prefix matching for explicit skill selection.
    Returns a SkillRoutingResult with all execution parameters set.
    """
    result = SkillRoutingResult()

    # Parse @skill: prefix
    explicit_skill, clean_content = parse_skill_prefix(content)
    result.clean_content = clean_content

    if explicit_skill:
        logger.info(f"Session {session_id}: explicit skill reference: {explicit_skill}")

    # Try explicit skill match
    if explicit_skill and skill_registry:
        try:
            matched_skill = skill_registry.get(explicit_skill)
            result.skill_name = explicit_skill
            result.skill_config = matched_skill.config
            result.skill_tools = matched_skill.tools or []
            result.matched = True
            result.match_method = "explicit"
            result.match_confidence = 1.0
            logger.info(f"Session {session_id}: using explicit skill '{explicit_skill}'")
        except (ValueError, KeyError, TypeError, RuntimeError) as e:
            logger.warning(
                f"Session {session_id}: explicit skill '{explicit_skill}' not found: {e}"
            )
            # Reset so we don't enter skill branch with stale data
            result.skill_name = None
            result.skill_config = None

    # Determine execution parameters
    if result.matched and result.skill_config:
        skill_prompt = build_skill_system_prompt(result.skill_config)
        result.system_prompt = skill_prompt or default_system_prompt

        # Merge skill tools with agent tools, deduplicating by name
        agent_tools = agent_tool_registry.list_tools() if agent_tool_registry else default_tools
        seen_names = set()
        merged_tools = []
        for tool in result.skill_tools + agent_tools:
            if tool.name not in seen_names:
                seen_names.add(tool.name)
                merged_tools.append(tool)
        # U5: 渐进式技能加载 — disclosure_level == 0 时注入 skill_detail 工具
        if getattr(result.skill_config, "disclosure_level", 1) == 0 and skill_registry is not None:
            from agentkit.skills.skill_detail import SkillDetailTool

            if "skill_detail" not in seen_names:
                merged_tools.append(SkillDetailTool(skill_registry=skill_registry))
                seen_names.add("skill_detail")
        result.tools = merged_tools

        result.model = (
            result.skill_config.llm.get("model", default_model)
            if result.skill_config.llm
            else default_model
        )
        result.agent_name = result.skill_name
        # Map skill.config.execution_mode to ExecutionMode enum
        result.execution_mode = _resolve_execution_mode(result.skill_config)
    else:
        result.system_prompt = default_system_prompt
        result.tools = default_tools
        result.model = default_model
        result.agent_name = default_agent_name
        # No skill matched — if we have tools, use ReAct; otherwise direct chat
        result.execution_mode = ExecutionMode.REACT if default_tools else ExecutionMode.DIRECT_CHAT

    # Append available tools to system prompt only when execution mode supports tool calls
    # DIRECT_CHAT mode has no tool execution loop — injecting tool instructions would
    # cause the LLM to output unparseable tool call JSON as plain text
    if result.tools and result.execution_mode != ExecutionMode.DIRECT_CHAT:
        tools_desc = _build_tools_description(result.tools)
        tool_instruction = (
            "\n\n## Tool Usage\n"
            "You have access to the following tools. When you need to use a tool, "
            "respond with a tool call in the format specified by the system.\n"
            "Never make up information or guess answers when you can use a tool to find the answer.\n"
            "Always prefer using tools over guessing.\n"
        )
        if result.system_prompt:
            result.system_prompt += f"{tool_instruction}\n## Available Tools\n{tools_desc}"
        else:
            result.system_prompt = f"{tool_instruction}\n## Available Tools\n{tools_desc}"

    return result


def _build_tools_description(tools: list[object]) -> str:
    """Build a text description of tools for the system prompt."""
    lines = []
    for tool in tools:
        desc = getattr(tool, "description", "")
        lines.append(f"- **{tool.name}**: {desc}")
        schema = getattr(tool, "input_schema", None)
        if schema and "properties" in schema:
            params = list(schema["properties"].keys())
            if params:
                lines[-1] += f" (parameters: {', '.join(params)})"
    return "\n".join(lines)
