"""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 json
import logging
import re
from dataclasses import dataclass, field
from typing import Any

from agentkit.telemetry.tracer import get_tracer

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}$")


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: Any = None
    skill_tools: list = field(default_factory=list)
    clean_content: str = ""
    system_prompt: str | None = None
    tools: list = 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


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


def build_skill_system_prompt(skill_config) -> str | None:
    """Build system prompt from skill config's prompt section."""
    if not skill_config or not skill_config.prompt:
        return None
    prompt_parts = []
    for key in ("identity", "context", "instructions", "constraints", "output_format"):
        val = skill_config.prompt.get(key)
        if val:
            prompt_parts.append(val)
    return "\n\n".join(prompt_parts) if prompt_parts else None


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

    This is the shared entry point used by both GUI WebSocket chat and CLI chat.
    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 Exception 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

    # Try IntentRouter if no explicit match
    if not result.matched and skill_registry and intent_router:
        skills = skill_registry.list_skills()
        routable_skills = [s for s in skills if s.config.intent.keywords]
        if routable_skills:
            try:
                routing_result = await intent_router.route(
                    input_data={"content": clean_content},
                    skills=routable_skills,
                )
                if routing_result and routing_result.confidence >= 0.5:
                    skill_name = routing_result.matched_skill
                    try:
                        matched_skill = skill_registry.get(skill_name)
                        skill_config = matched_skill.config

                        # Check if matched skill can handle tool-calling tasks.
                        # Direct-mode agents with no tools cannot execute tasks
                        # that require tool use (shell, search, etc.).
                        # If the task content suggests tool needs, fall through
                        # to default agent which has full tool access.
                        execution_mode = getattr(skill_config, "execution_mode", "react")
                        skill_tools = matched_skill.tools or []
                        if execution_mode == "direct" and not skill_tools:
                            # Direct agent matched but has no tools — check if
                            # the task might need tools. If so, skip this match
                            # and let it fall through to default agent.
                            tool_hints = [
                                "执行", "运行", "命令", "终端", "shell", "bash",
                                "搜索", "查找", "联网", "搜索", "search",
                                "安装", "部署", "启动", "停止", "重启",
                                "文件", "目录", "创建", "删除", "修改",
                                "run", "execute", "install", "deploy",
                                "start", "stop", "restart", "file",
                            ]
                            content_lower = clean_content.lower()
                            needs_tools = any(h in content_lower for h in tool_hints)
                            if needs_tools:
                                logger.info(
                                    f"Session {session_id}: skill '{skill_name}' is direct-mode "
                                    f"but task may need tools, falling through to default agent"
                                )
                                # Don't set result.matched, let it fall through
                            else:
                                result.skill_name = skill_name
                                result.skill_config = skill_config
                                result.skill_tools = skill_tools
                                result.matched = True
                                result.match_method = routing_result.method
                                result.match_confidence = routing_result.confidence
                        else:
                            result.skill_name = skill_name
                            result.skill_config = skill_config
                            result.skill_tools = skill_tools
                            result.matched = True
                            result.match_method = routing_result.method
                            result.match_confidence = routing_result.confidence

                        if result.matched:
                            logger.info(
                                f"Session {session_id}: routed to skill '{skill_name}' "
                                f"via {routing_result.method} (confidence={routing_result.confidence})"
                            )
                    except Exception as e:
                        logger.warning(f"Session {session_id}: skill '{skill_name}' found by router but not in registry: {e}")
            except Exception as e:
                logger.warning(f"Skill routing failed for session {session_id}: {e}")

    # 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)
        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
    else:
        result.system_prompt = default_system_prompt
        result.tools = default_tools
        result.model = default_model
        result.agent_name = default_agent_name

    # Append available tools to system prompt so LLM knows what it can call
    if result.tools:
        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) -> 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)


# ---------------------------------------------------------------------------
# CostAwareRouter - 三层成本感知路由
# ---------------------------------------------------------------------------

_GREETING_RE = re.compile(
    r"^(你好|hi|hello|hey|嗨|哈喽|早上好|下午好|晚上好|good morning|good afternoon|good evening)\s*[!！.。?？]*$",
    re.IGNORECASE,
)

_CHAT_MODE_RE = re.compile(
    r"^(谢谢|感谢|thanks|thank you|ok|好的|嗯|对|是|不是|没关系|再见|bye|goodbye)\s*[!！.。?？]*$",
    re.IGNORECASE,
)


def _tokenize_content(content: str) -> list[str]:
    """Tokenize content for capability matching. Supports Chinese and English."""
    # 1. Split by punctuation and whitespace
    segments = re.split(r'[\s,，。！？、；：\n]+', content)

    # 2. For long Chinese segments, add 2-gram supplements
    tokens = []
    for seg in segments:
        if len(seg) <= 4:
            tokens.append(seg)
        else:
            tokens.append(seg)
            # Add 2-grams for Chinese compound words
            for i in range(len(seg) - 1):
                bigram = seg[i:i+2]
                if all('\u4e00' <= c <= '\u9fff' for c in bigram):
                    tokens.append(bigram)

    # 3. Filter stopwords
    stopwords = {"的", "了", "是", "在", "和", "与", "也", "都", "就", "要", "会", "我", "你", "他", "这", "那", "有", "没", "不"}
    tokens = [t for t in tokens if t not in stopwords and len(t) > 1][:10]

    return tokens


class CostAwareRouter:
    """三层成本感知路由器。

    Layer 0: 规则匹配（零成本）— @skill: 前缀 / 问候 / 简单对话
    Layer 1: LLM 快速分类（~100 tokens）— 复杂度评估 + IntentRouter
    Layer 2: 能力匹配 / 拍卖（可选）— 高复杂度任务委派给最佳 Agent
    """

    def __init__(
        self,
        llm_gateway: Any = None,
        model: str = "default",
        org_context: Any = None,
        auction_enabled: bool = False,
    ):
        self._llm_gateway = llm_gateway
        self._model = model
        self._org_context = org_context
        self._auction_enabled = auction_enabled

    # -- Layer 0: Rule-based (zero cost) ------------------------------------

    def _match_layer0(self, content: str) -> tuple[str | None, str]:
        """Layer 0 规则匹配。

        Returns:
            (match_type, clean_content) — match_type 为 None 表示未命中。
        """
        # @skill: 显式前缀
        explicit_skill, clean = parse_skill_prefix(content)
        if explicit_skill:
            return "explicit_skill", clean

        # 问候模式
        stripped = content.strip()
        if _GREETING_RE.match(stripped):
            return "greeting", stripped

        # 简单对话模式
        if _CHAT_MODE_RE.match(stripped):
            return "chat_mode", stripped

        return None, stripped

    # -- Layer 1: LLM quick classify (~100 tokens) -------------------------

    async def quick_classify(self, content: str) -> float:
        """使用 LLM 快速评估用户请求的复杂度 (0.0-1.0)。

        当 LLM Gateway 不可用或解析失败时，返回默认中等复杂度 0.5。
        """
        if self._llm_gateway is None:
            return 0.5

        prompt = (
            'You are a complexity classifier. Rate the complexity of the user request on a scale of 0.0 to 1.0.\n'
            '0.0 = trivial greeting, 0.3 = simple question, 0.5 = moderate task, '
            '0.7 = complex multi-step task, 1.0 = very complex research task.\n\n'
            '---BEGIN USER REQUEST---\n'
            f'{content}\n'
            '---END USER REQUEST---\n\n'
            'Respond ONLY with a JSON object: {"complexity": <float>}'
        )
        try:
            response = await self._llm_gateway.chat(
                messages=[{"role": "user", "content": prompt}],
                model=self._model,
            )
            data = json.loads(response.content.strip())
            complexity = float(data.get("complexity", 0.5))
            return max(0.0, min(1.0, complexity))
        except Exception as e:
            logger.warning(f"CostAwareRouter quick_classify failed: {e}")
            return 0.5

    # -- Layer 2: Capability matching / Auction (optional) -----------------

    async def _route_layer2(
        self,
        content: str,
        skill_registry: Any,
        intent_router: Any,
        default_tools: list,
        default_system_prompt: str | None,
        default_model: str,
        default_agent_name: str,
        agent_tool_registry: Any = None,
        session_id: str = "",
        complexity: float = 0.0,
        trace: list[dict] | None = None,
    ) -> SkillRoutingResult:
        """Layer 2: 高复杂度任务通过 org_context.find_best_agent 路由。"""
        if self._org_context is not None and hasattr(self._org_context, "find_best_agent"):
            try:
                # Extract capability-like keywords from content for matching
                # find_best_agent expects list[str] of required capabilities
                content_words = _tokenize_content(content)
                best_agent = self._org_context.find_best_agent(required_capabilities=content_words)
                if best_agent is not None:
                    agent_name = best_agent if isinstance(best_agent, str) else getattr(best_agent, "name", str(best_agent))
                    result = SkillRoutingResult(
                        clean_content=content,
                        matched=True,
                        match_method="capability",
                        match_confidence=0.8,
                        agent_name=agent_name,
                        model=default_model,
                        system_prompt=default_system_prompt,
                        tools=default_tools,
                        complexity=complexity,
                    )
                    if trace is not None:
                        trace.append({
                            "layer": 2,
                            "method": "capability",
                            "agent_name": agent_name,
                            "complexity": complexity,
                        })
                    return result
            except Exception as e:
                logger.warning(f"CostAwareRouter Layer 2 org_context.find_best_agent failed: {e}")

        # Fallback: 使用 IntentRouter
        result = await resolve_skill_routing(
            content=content,
            skill_registry=skill_registry,
            intent_router=intent_router,
            default_tools=default_tools,
            default_system_prompt=default_system_prompt,
            default_model=default_model,
            default_agent_name=default_agent_name,
            agent_tool_registry=agent_tool_registry,
            session_id=session_id,
        )
        result.complexity = complexity
        if trace is not None:
            trace.append({
                "layer": 2,
                "method": "intent_router_fallback",
                "complexity": complexity,
            })
        return result

    # -- Main entry point ---------------------------------------------------

    async def route(
        self,
        content: str,
        skill_registry: Any,
        intent_router: Any,
        default_tools: list,
        default_system_prompt: str | None,
        default_model: str = "default",
        default_agent_name: str = "default",
        agent_tool_registry: Any = None,
        session_id: str = "",
        transparency: str = "SILENT",
    ) -> SkillRoutingResult:
        """三层成本感知路由主入口。

        Args:
            content: 用户输入内容
            skill_registry: Skill 注册表
            intent_router: IntentRouter 实例
            default_tools: 默认工具列表
            default_system_prompt: 默认系统提示词
            default_model: 默认模型
            default_agent_name: 默认 Agent 名称
            agent_tool_registry: Agent 工具注册表
            session_id: 会话 ID
            transparency: 透明度级别 (SILENT / VERBOSE / TRACE)

        Returns:
            SkillRoutingResult 包含路由结果和追踪信息
        """
        trace: list[dict] = []

        tracer = get_tracer()
        with tracer.start_span("router.route") as span:
            span.set_attribute("input.length", len(content))

            # ---- Layer 0: Rule-based (zero cost) ----
            match_type, clean_content = self._match_layer0(content)

            if match_type == "explicit_skill":
                result = await resolve_skill_routing(
                    content=content,
                    skill_registry=skill_registry,
                    intent_router=intent_router,
                    default_tools=default_tools,
                    default_system_prompt=default_system_prompt,
                    default_model=default_model,
                    default_agent_name=default_agent_name,
                    agent_tool_registry=agent_tool_registry,
                    session_id=session_id,
                )
                result.match_method = result.match_method or "explicit_skill"
                result.complexity = 0.0
                trace.append({
                    "layer": 0,
                    "method": "explicit_skill",
                    "matched": result.matched,
                    "cost": "zero",
                })
                result.execution_trace = trace if transparency != "SILENT" else []
                result.transparency_level = transparency
                span.set_attribute("route.layer", result.match_method or "explicit_skill")
                span.set_attribute("route.target", result.skill_name or "default")
                return result

            if match_type in ("greeting", "chat_mode"):
                result = SkillRoutingResult(
                    clean_content=clean_content,
                    system_prompt=default_system_prompt,
                    tools=default_tools,
                    model=default_model,
                    agent_name=default_agent_name,
                    matched=False,
                    match_method=match_type,
                    match_confidence=1.0,
                    complexity=0.0,
                )
                trace.append({
                    "layer": 0,
                    "method": match_type,
                    "matched": False,
                    "cost": "zero",
                })
                result.execution_trace = trace if transparency != "SILENT" else []
                result.transparency_level = transparency
                span.set_attribute("route.layer", match_type)
                span.set_attribute("route.target", "default")
                return result

            # ---- Layer 1: LLM quick classify (~100 tokens) ----
            complexity = await self.quick_classify(clean_content)
            trace.append({
                "layer": 1,
                "method": "quick_classify",
                "complexity": complexity,
            })

            # Low complexity → default agent
            if complexity < 0.3:
                result = SkillRoutingResult(
                    clean_content=clean_content,
                    system_prompt=default_system_prompt,
                    tools=default_tools,
                    model=default_model,
                    agent_name=default_agent_name,
                    matched=False,
                    match_method="low_complexity",
                    match_confidence=1.0 - complexity,
                    complexity=complexity,
                )
                trace.append({
                    "layer": 1,
                    "method": "low_complexity",
                    "complexity": complexity,
                    "routed_to": "default",
                })
                result.execution_trace = trace if transparency != "SILENT" else []
                result.transparency_level = transparency
                span.set_attribute("route.layer", "low_complexity")
                span.set_attribute("route.target", "default")
                return result

            # Medium complexity → IntentRouter via resolve_skill_routing
            if complexity <= 0.7:
                result = await resolve_skill_routing(
                    content=content,
                    skill_registry=skill_registry,
                    intent_router=intent_router,
                    default_tools=default_tools,
                    default_system_prompt=default_system_prompt,
                    default_model=default_model,
                    default_agent_name=default_agent_name,
                    agent_tool_registry=agent_tool_registry,
                    session_id=session_id,
                )
                result.complexity = complexity
                trace.append({
                    "layer": 1,
                    "method": "intent_router",
                    "complexity": complexity,
                    "matched": result.matched,
                })
                result.execution_trace = trace if transparency != "SILENT" else []
                result.transparency_level = transparency
                span.set_attribute("route.layer", result.match_method or "intent_router")
                span.set_attribute("route.target", result.skill_name or "default")
                return result

            # ---- Layer 2: Capability matching / Auction (high complexity) ----
            trace.append({
                "layer": 2,
                "method": "capability_or_auction",
                "complexity": complexity,
                "auction_enabled": self._auction_enabled,
            })
            result = await self._route_layer2(
                content=content,
                skill_registry=skill_registry,
                intent_router=intent_router,
                default_tools=default_tools,
                default_system_prompt=default_system_prompt,
                default_model=default_model,
                default_agent_name=default_agent_name,
                agent_tool_registry=agent_tool_registry,
                session_id=session_id,
                complexity=complexity,
                trace=trace,
            )
            result.execution_trace = trace if transparency != "SILENT" else []
            result.transparency_level = transparency
            span.set_attribute("route.layer", result.match_method or "capability")
            span.set_attribute("route.target", result.skill_name or result.agent_name or "default")
            return result
