from __future__ import annotations

import json
import re
from typing import Protocol

from app.config import Settings
from app.schemas import (
    Evidence,
    KnowledgeType,
    PlanStep,
    QualityGrade,
    RetrievalPlan,
    RouteDecision,
    RouteName,
    SOURCE_TO_KNOWLEDGE,
    TaskType,
)


SERVICE_NAME_PATTERN = re.compile(
    r"aliyun[_ -]?ssl[_ -]?vpn|SSL[-_ ]?VPN", re.IGNORECASE
)
# \b 在 Python re 与中文字符之间不构成 word boundary，因此中文部分走纯子串匹配。
ERROR_TYPE_PATTERN = re.compile(
    r"auth[_-]?timeout|认证超时", re.IGNORECASE
)
CLIENT_OS_PATTERN = re.compile(r"Windows|macOS|Linux", re.IGNORECASE)
TIME_RANGE_PATTERN = re.compile(r"(\d+)\s*(?:天|d)\b", re.IGNORECASE)
USER_ID_PATTERN = re.compile(r"\bU\d{4}\b", re.IGNORECASE)

RESTRICTED_TERMS = (
    "所有员工手机号",
    "员工密码",
    "员工薪资",
    "导出全部",
    "删除所有",
    "关闭防火墙",
    "禁用杀毒",
)
DIRECT_TERMS = ("改写", "润色", "翻译", "缩短", "转成")


# 本作业固定只处理 aliyun_ssl_vpn 与 auth_timeout；不在白名单的服务/故障一律拒绝或澄清。
ALLOWED_SERVICE = "aliyun_ssl_vpn"
ALLOWED_ERROR_TYPE = "auth_timeout"
VENDOR_DOMAIN = "help.aliyun.com"


def extract_service_name(query: str) -> str:
    """把 SSL-VPN 的所有变体（含"阿里云 SSL-VPN"、"SSL-VPN"、"aliyun_ssl_vpn"）映射到规范名。"""
    if SERVICE_NAME_PATTERN.search(query):
        return ALLOWED_SERVICE
    return ""


def extract_error_type(query: str) -> str:
    """识别认证超时类故障并返回规范名 `auth_timeout`。"""
    if ERROR_TYPE_PATTERN.search(query):
        return ALLOWED_ERROR_TYPE
    return ""


def extract_client_os(query: str) -> str:
    match = CLIENT_OS_PATTERN.search(query)
    return match.group(0) if match else ""


def extract_time_range_days(query: str) -> int:
    match = TIME_RANGE_PATTERN.search(query)
    if not match:
        return 30
    days = int(match.group(1))
    # 与 SQL 层的 days ∈ [1,365] 对齐；超过 365 直接夹紧。
    return max(1, min(days, 365))


class DecisionEngine(Protocol):
    def route(self, query: str, max_rounds: int) -> RouteDecision: ...

    def plan(self, query: str, decision: RouteDecision) -> RetrievalPlan: ...

    def grade(
        self,
        query: str,
        decision: RouteDecision,
        evidence: list[Evidence],
        current_round: int,
        max_rounds: int,
        min_score: float,
    ) -> QualityGrade: ...

    def rewrite(self, query: str, grade: QualityGrade) -> str: ...

    def answer(self, query: str, evidence: list[Evidence], partial: bool = False) -> str: ...


class DemoDecisionEngine:
    """确定性 IT 域演示引擎，便于无 API Key 启动、调试和自动化测试。"""

    SQL_TERMS = (
        "几次", "多少", "发生次数", "统计", "工单", "次数", "几天", "天里",
    )
    DOC_TERMS = (
        "排查", "排障", "手册", "步骤", "怎么", "如何", "流程", "顺序",
    )
    WEB_TERMS = (
        "官网", "厂商", "公告", "最新", "公共服务", "服务状态", "运维事件",
        "公共异常", "服务异常", "服务公告",
    )

    def route(self, query: str, max_rounds: int) -> RouteDecision:
        stripped = query.strip()

        # 安全门控：敏感请求或被禁用的写操作一律 REFUSE。
        if any(term in stripped for term in RESTRICTED_TERMS):
            return RouteDecision(
                needs_retrieval=False,
                intent="restricted_request",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.REFUSE],
                confidence="high",
                reason_code="SECURITY_POLICY",
                max_rounds=max_rounds,
            )

        # 文本改写类请求无需任何检索。
        if any(term in stripped for term in DIRECT_TERMS):
            return RouteDecision(
                needs_retrieval=False,
                intent="text_transformation",
                task_type=TaskType.TEXT_TRANSFORM,
                routes=[RouteName.DIRECT_ANSWER],
                confidence="high",
                reason_code="NO_EXTERNAL_KNOWLEDGE_REQUIRED",
                max_rounds=max_rounds,
            )

        service_name = extract_service_name(stripped)
        error_type = extract_error_type(stripped)
        client_os = extract_client_os(stripped)
        time_range_days = extract_time_range_days(stripped)

        # 防御性检查：业务用户编号不应出现在 IT 域问题里（防止模板残留）。
        if USER_ID_PATTERN.search(stripped):
            return RouteDecision(
                needs_retrieval=False,
                intent="invalid_credential",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.REFUSE],
                confidence="high",
                reason_code="USER_ID_NOT_APPLICABLE",
                max_rounds=max_rounds,
            )

        needs_sql = any(term in stripped for term in self.SQL_TERMS)
        needs_docs = any(term in stripped for term in self.DOC_TERMS)
        needs_web = any(term in stripped for term in self.WEB_TERMS)

        # 任何路由都要求 service_name 与 error_type 都已显式给出；否则 CLARIFY。
        if (needs_sql or needs_docs or needs_web) and not service_name:
            return RouteDecision(
                needs_retrieval=False,
                intent="missing_service_name",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.CLARIFY],
                confidence="high",
                reason_code="MISSING_SERVICE_NAME",
                max_rounds=max_rounds,
            )
        if (needs_sql or needs_docs or needs_web) and not error_type:
            return RouteDecision(
                needs_retrieval=False,
                intent="missing_error_type",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.CLARIFY],
                confidence="high",
                reason_code="MISSING_ERROR_TYPE",
                max_rounds=max_rounds,
            )

        # 路由决策：每个意图独立判断后取并集，三类需求齐备即三源并行。
        routes: list[RouteName] = []
        knowledge_types: list[KnowledgeType] = []
        if needs_sql:
            routes.append(RouteName.SQL_QUERY)
            knowledge_types.append(KnowledgeType.STRUCTURED)
        if needs_docs:
            routes.append(RouteName.MILVUS_SEARCH)
            knowledge_types.append(KnowledgeType.INTERNAL_DOC)
        if needs_web:
            routes.append(RouteName.WEB_SEARCH)
            knowledge_types.append(KnowledgeType.REALTIME)

        if not routes:
            # 没有命中任何意图关键词 → 直接要求澄清，避免随机路由。
            return RouteDecision(
                needs_retrieval=False,
                intent="unclear_intent",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.CLARIFY],
                confidence="high",
                reason_code="INTENT_UNCLEAR",
                max_rounds=max_rounds,
            )

        filters = {
            "service_name": service_name,
            "error_type": error_type,
            "client_os": client_os,
            "days": time_range_days,
        }
        filters = {key: value for key, value in filters.items() if value}

        if len(routes) >= 2:
            return RouteDecision(
                needs_retrieval=True,
                intent="multi_source_diagnosis",
                task_type=TaskType.DIAGNOSE,
                routes=routes,
                knowledge_types=knowledge_types,
                requires_decomposition=True,
                confidence="high",
                reason_code="MULTIPLE_DATA_SOURCES_REQUIRED",
                filters=filters,
                max_rounds=max_rounds,
            )
        if needs_sql:
            return RouteDecision(
                needs_retrieval=True,
                intent="incident_statistics",
                task_type=TaskType.HISTORICAL_QUERY,
                routes=routes,
                knowledge_types=knowledge_types,
                confidence="high",
                reason_code="EXACT_AGGREGATION_REQUIRED",
                filters=filters,
                max_rounds=max_rounds,
            )
        if needs_web:
            return RouteDecision(
                needs_retrieval=True,
                intent="vendor_status_check",
                task_type=TaskType.VENDOR_STATUS,
                routes=routes,
                knowledge_types=knowledge_types,
                confidence="high",
                reason_code="CURRENT_PUBLIC_INFORMATION_REQUIRED",
                filters=filters,
                max_rounds=max_rounds,
            )
        return RouteDecision(
            needs_retrieval=True,
            intent="internal_doc_qa",
            task_type=TaskType.DOCS_QA,
            routes=routes,
            knowledge_types=knowledge_types,
            confidence="high" if client_os else "medium",
            reason_code="INTERNAL_DOCUMENT_REQUIRED",
            filters=filters,
            max_rounds=max_rounds,
        )

    def plan(self, query: str, decision: RouteDecision) -> RetrievalPlan:
        service_name = str(decision.filters.get("service_name", ALLOWED_SERVICE))
        error_type = str(decision.filters.get("error_type", ALLOWED_ERROR_TYPE))
        client_os = str(decision.filters.get("client_os", ""))
        days = int(decision.filters.get("days", 30))
        steps: list[PlanStep] = []

        # 三类只读任务互不依赖，全部并行执行；depends_on 留空即可。
        if RouteName.MILVUS_SEARCH in decision.routes:
            steps.append(
                PlanStep(
                    id="search_troubleshooting_doc",
                    tool=RouteName.MILVUS_SEARCH,
                    # 直接用服务名 + 错误类型组合检索，比原始用户问题更聚焦。
                    query=f"{service_name} {error_type} 排查步骤 {client_os}".strip(),
                    arguments={
                        "service_name": service_name,
                        "client_os": client_os,
                        "doc_type": "troubleshooting_guide",
                        "top_k": 4,
                    },
                )
            )
        if RouteName.SQL_QUERY in decision.routes:
            steps.append(
                PlanStep(
                    id="query_incident_statistics",
                    tool=RouteName.SQL_QUERY,
                    query=(
                        f"统计 {service_name} 最近 {days} 天"
                        f"{error_type} 的有效工单"
                    ),
                    arguments={
                        "service_name": service_name,
                        "error_type": error_type,
                        "days": days,
                        "client_os": client_os,
                    },
                )
            )
        if RouteName.WEB_SEARCH in decision.routes:
            # 受控搜索词：固定 site 限定 + 产品名 + 故障类型 + 最新状态诉求，
            # 不直接把用户自然语言送出去。
            vendor_status_query = (
                f'site:{VENDOR_DOMAIN}/zh/vpn 阿里云 SSL-VPN '
                f'{error_type} 运维事件 最新公告'
            )
            steps.append(
                PlanStep(
                    id="search_vendor_status",
                    tool=RouteName.WEB_SEARCH,
                    query=vendor_status_query,
                    arguments={"max_results": 3},
                )
            )
        return RetrievalPlan(goal=query, steps=steps)

    def grade(
        self,
        query: str,
        decision: RouteDecision,
        evidence: list[Evidence],
        current_round: int,
        max_rounds: int,
        min_score: float,
    ) -> QualityGrade:
        # 先做确定性来源覆盖检查，再判断语义质量与冲突。
        required_sources = set()
        if RouteName.SQL_QUERY in decision.routes:
            required_sources.add("sql")
        if RouteName.MILVUS_SEARCH in decision.routes:
            required_sources.add("milvus")
        if RouteName.WEB_SEARCH in decision.routes:
            required_sources.add("web")

        available_sources = {item.source_type for item in evidence}
        missing = sorted(required_sources - available_sources)

        vector_evidence = [item for item in evidence if item.source_type == "milvus"]
        # 至少一条向量结果得分不低于 min_score 才视为相关。
        vector_relevant = any(
            item.score is not None and item.score >= min_score
            for item in vector_evidence
        )
        if "milvus" not in required_sources:
            vector_relevant = True

        # 厂商状态缺失时必须返回 partial：答案不能排除公共服务异常。
        if "web" in required_sources and "web" not in available_sources:
            return QualityGrade(
                relevant=bool(evidence),
                sufficient=False,
                missing_aspects=["厂商官网状态"],
                conflict=False,
                recommended_action="partial",
                reason="厂商官网状态缺失，证据不完整，必须以部分结论给出。",
            )

        # 厂商与内部文档冲突时标记冲突，但不阻塞终止。
        conflict = self._detect_conflict(evidence)

        if not missing and vector_relevant:
            action = "accept" if not conflict else "accept"
            return QualityGrade(
                relevant=True,
                sufficient=True,
                missing_aspects=[],
                conflict=conflict,
                recommended_action=action,
                reason=(
                    "所需数据源均返回证据，且 Milvus 命中相关结果。"
                    if not conflict
                    else "证据完整但厂商公告与内部手册结论冲突，需在答案中并列两条路径。"
                ),
            )

        # 缺失关键证据但还有预算 → 改写 Query 重新检索。
        if current_round < max_rounds:
            return QualityGrade(
                relevant=bool(evidence),
                sufficient=False,
                missing_aspects=missing or ["相关排障手册"],
                conflict=conflict,
                recommended_action="rewrite_query",
                reason="证据不完整，允许改写后重试。",
            )

        return QualityGrade(
            relevant=bool(evidence),
            sufficient=False,
            missing_aspects=missing or ["相关排障手册"],
            conflict=conflict,
            recommended_action="stop",
            reason="达到最大检索轮数，停止继续调用工具。",
        )

    @staticmethod
    def _detect_conflict(evidence: list[Evidence]) -> bool:
        """轻量级冲突检测：厂商公告与内部结论出现对立关键词。"""
        has_vendor_issue = False
        has_vendor_normal = False
        for item in evidence:
            if item.source_type != "web":
                continue
            text = item.content.lower()
            if "服务异常" in text or "故障公告" in text or "中断" in text:
                has_vendor_issue = True
            if "未发现" in text or "无异常" in text or "正常" in text:
                has_vendor_normal = True
        # 没有任何厂商证据时不应判为冲突。
        return has_vendor_issue and has_vendor_normal

    def rewrite(self, query: str, grade: QualityGrade) -> str:
        missing = "、".join(grade.missing_aspects) if grade.missing_aspects else "相关资料"
        suffix = f"阿里云 SSL-VPN auth_timeout 排障手册 厂商公告 {missing}".strip()
        return f"{query}；补充检索条件：{suffix}"

    def answer(self, query: str, evidence: list[Evidence], partial: bool = False) -> str:
        if not evidence:
            return "未获得能够支持回答的证据。请补充服务名称或故障现象后重试。"

        prefix = "当前仅能给出部分结论，公共服务状态仍待确认。" if partial else "基于已检索证据，结论如下。"
        lines = [prefix]
        sql_items = [item for item in evidence if item.source_type == "sql"]
        milvus_items = [item for item in evidence if item.source_type == "milvus"]
        web_items = [item for item in evidence if item.source_type == "web"]

        index = 1
        if sql_items:
            for item in sql_items:
                lines.append(f"- 历史工单：{item.content} [{index}]")
                index += 1
        if milvus_items:
            for item in milvus_items:
                lines.append(f"- 内部手册：{item.content} [{index}]")
                index += 1
        if web_items:
            for item in web_items:
                lines.append(f"- 厂商公告：{item.content} [{index}]")
                index += 1

        # 三类证据齐备时给出综合判断，让用户拿到行动指引而不是单纯堆事实。
        # query 同时含 SQL/Milvus/Web 时,意味着用户要的是"本地 vs 公共服务"的判断,
        # 我们就追加一句结论。
        if (
            not partial
            and sql_items
            and milvus_items
            and web_items
        ):
            lines.append(
                "- 综合判断：历史工单有可复现的本地处置记录(证书刷新/重建 Profile),"
                "厂商侧公告显示服务运行正常,倾向本地配置问题;请优先按内部手册"
                "步骤排查,公共服务异常可暂不升级。"
            )
        return "\n".join(lines)


class DeepSeekDecisionEngine(DemoDecisionEngine):
    """使用 DeepSeek 的 OpenAI 兼容接口完成路由、评分和答案生成。"""

    def __init__(self, settings: Settings) -> None:
        # 凭证只从 Settings 读取，并在创建 SDK 客户端时短暂解密。
        api_key = settings.deepseek_api_key.get_secret_value()
        if not api_key:
            raise RuntimeError("LLM_PROVIDER=deepseek 时必须配置 DEEPSEEK_API_KEY")
        try:
            from langchain_openai import ChatOpenAI
        except ImportError as exc:
            raise RuntimeError(
                "缺少 DeepSeek 依赖，请执行 uv sync --extra test --extra deepseek"
            ) from exc

        common_kwargs = {
            "model": settings.deepseek_model_name,
            "api_key": api_key,
            "base_url": settings.deepseek_base_url,
            "max_retries": 2,
        }

        # 最终答案可以使用 Thinking；开关由 .env 控制。
        answer_thinking = "enabled" if settings.deepseek_answer_thinking else "disabled"
        self.llm = ChatOpenAI(
            **common_kwargs,
            extra_body={"thinking": {"type": answer_thinking}},
        )

        # with_structured_output(function_calling) 会发送强制 tool_choice。
        # DeepSeek Thinking 模式不接受该组合，因此路由与评分必须关闭 Thinking。
        self.structured_llm = ChatOpenAI(
            **common_kwargs,
            temperature=0,
            extra_body={"thinking": {"type": "disabled"}},
        )
        self.router = self.structured_llm.with_structured_output(
            RouteDecision,
            method="function_calling",
        )
        self.grader = self.structured_llm.with_structured_output(
            QualityGrade,
            method="function_calling",
        )

    def route(self, query: str, max_rounds: int) -> RouteDecision:
        prompt = f"""
你是企业 IT 故障诊断助手路由器。只能输出符合 Schema 的结果。
可选路径：direct_answer、milvus_search、sql_query、web_search、clarify、refuse。
历史工单次数、统计、影响终端使用 sql_query；
内部排障手册使用 milvus_search；
厂商官网最新公告/服务状态使用 web_search（必须配合 site:help.aliyun.com 限定）。
filters 中只填写从原问题明确提取的 service_name、error_type、client_os、days。
缺少 service_name 或 error_type 时返回 clarify；越权请求返回 refuse。
最大检索轮数：{max_rounds}
用户问题：{query}
""".strip()
        result = self.router.invoke(prompt)
        # 结构化输出缺失时回退到确定性规则，路由不允许 None 继续走图。
        if result is None:
            return super().route(query, max_rounds)
        # 关键业务槽位使用确定性解析覆盖模型结果，避免模型改写服务身份。
        deterministic_filters = {
            key: value
            for key, value in {
                "service_name": extract_service_name(query),
                "error_type": extract_error_type(query),
                "client_os": extract_client_os(query),
                "days": extract_time_range_days(query),
            }.items()
            if value
        }
        result.filters = {**result.filters, **deterministic_filters}
        if (
            RouteName.SQL_QUERY in result.routes
            and "service_name" not in result.filters
        ):
            return RouteDecision(
                needs_retrieval=False,
                intent="missing_service_name",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.CLARIFY],
                confidence="high",
                reason_code="MISSING_SERVICE_NAME",
                max_rounds=max_rounds,
            )
        if (
            RouteName.MILVUS_SEARCH in result.routes
            and "error_type" not in result.filters
        ):
            return RouteDecision(
                needs_retrieval=False,
                intent="missing_error_type",
                task_type=TaskType.UNKNOWN,
                routes=[RouteName.CLARIFY],
                confidence="high",
                reason_code="MISSING_ERROR_TYPE",
                max_rounds=max_rounds,
            )
        result.max_rounds = max_rounds
        # 知识类型与数据源严格对齐，避免模型漏报。
        result.knowledge_types = [
            SOURCE_TO_KNOWLEDGE[route] for route in result.routes if route in SOURCE_TO_KNOWLEDGE
        ]
        return result

    def grade(
        self,
        query: str,
        decision: RouteDecision,
        evidence: list[Evidence],
        current_round: int,
        max_rounds: int,
        min_score: float,
    ) -> QualityGrade:
        prompt = f"""
判断证据是否足以回答 IT 故障诊断问题。recommended_action 只能是 accept、rewrite_query、stop 或 partial。
当前轮次：{current_round}/{max_rounds}
问题：{query}
路由：{decision.model_dump_json()}
证据：{json.dumps([item.model_dump() for item in evidence], ensure_ascii=False)}
""".strip()
        result = self.grader.invoke(prompt)
        if result is None:
            return super().grade(
                query=query,
                decision=decision,
                evidence=evidence,
                current_round=current_round,
                max_rounds=max_rounds,
                min_score=min_score,
            )
        return result

    def answer(self, query: str, evidence: list[Evidence], partial: bool = False) -> str:
        prompt = f"""
严格依据证据回答 IT 故障诊断问题，不得补充证据之外的事实。
每个关键结论必须使用 [序号] 引用对应证据。
若厂商公告缺失必须明确告知"无法确认公共服务状态"。
问题：{query}
是否为部分证据：{partial}
证据：{json.dumps([item.model_dump() for item in evidence], ensure_ascii=False)}
""".strip()
        return str(self.llm.invoke(prompt).content)


def create_decision_engine(settings: Settings) -> DecisionEngine:
    if settings.llm_provider == "demo":
        return DemoDecisionEngine()
    if settings.llm_provider == "deepseek":
        return DeepSeekDecisionEngine(settings)
    raise ValueError(f"不支持的 LLM_PROVIDER: {settings.llm_provider}")