from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass
from typing import Any
from urllib.parse import urlencode

import httpx
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.config import get_settings
from app.models.ai_config import ModelConfig, SystemConfig
from app.services.external_errors import ExternalServiceError
from app.services.model_routing_service import ModelRoute, ModelRoutingService, ModelScenario
from app.services.rag_service import NO_HIT_ANSWER, RagResult
from app.services.secret_service import SecretService


@dataclass(frozen=True)
class ModelCompletion:
    answer: str
    model_id: int | None
    model_name: str
    input_token: int
    output_token: int
    route_reason: str | None = None
    question_type: str | None = None


class ModelClientService:
    @staticmethod
    def complete(db: Session, rag_result: RagResult) -> ModelCompletion:
        route = ModelRoutingService.resolve_chat(
            db,
            [chunk.knowledge_type for chunk in rag_result.chunks],
            rag_result.question,
        )
        model = route.model
        mock_model_enabled = _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled)
        if mock_model_enabled:
            model_name = model.model_name if model is not None else "mock-model"
            answer = _mock_answer(rag_result)
        else:
            if model is None:
                raise ExternalServiceError("未启用可用模型，请先在模型管理中启用一个模型。", provider="model")
            model_name = model.model_name
            try:
                answer = _call_configured_model(model, rag_result, allow_no_hit=True)
            except ExternalServiceError as primary_error:
                fallback = ModelRoutingService.default_model(db)
                if fallback is None or fallback.id == model.id:
                    _annotate_model_error(primary_error, model, route.reason, route.question_type)
                    raise
                fallback_reason = f"{route.reason}；场景模型调用失败，运行时回退默认主模型"
                try:
                    answer = _call_configured_model(fallback, rag_result, allow_no_hit=True)
                except ExternalServiceError as fallback_error:
                    _annotate_model_error(
                        fallback_error,
                        fallback,
                        f"{fallback_reason}；默认主模型调用仍失败",
                        route.question_type,
                    )
                    raise
                model = fallback
                model_name = fallback.model_name
                route = ModelRoute(
                    model=fallback,
                    scenario=route.scenario,
                    reason=fallback_reason,
                    fallback_used=True,
                    question_type=route.question_type,
                )
        return ModelCompletion(
            answer=answer,
            model_id=model.id if model is not None else None,
            model_name=model_name,
            input_token=_rough_token_count(rag_result.prompt),
            output_token=_rough_token_count(answer),
            route_reason=route.reason,
            question_type=route.question_type,
        )

    @staticmethod
    def summarize(
        db: Session,
        messages_text: str,
        previous_summary: str | None = None,
    ) -> str | None:
        """调用模型生成对话摘要，失败时返回 None（不阻断主流程）"""
        try:
            return ModelClientService.summarize_or_raise(db, messages_text, previous_summary)
        except Exception:
            return None

    @staticmethod
    def summarize_or_raise(
        db: Session,
        messages_text: str,
        previous_summary: str | None = None,
    ) -> str:
        model = ModelClientService._get_enabled_model(db)
        if _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled):
            combined = "\n".join(part for part in [previous_summary, messages_text] if part).strip()
            return combined[-1000:]
        if model is None:
            raise ExternalServiceError("未启用可用于生成历史摘要的模型", provider="model")
        return _generate_summary(model, messages_text, previous_summary)

    @staticmethod
    def generate_text_or_raise(db: Session, prompt: str) -> str:
        model = ModelClientService._get_enabled_model(db)
        if _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled):
            return prompt.strip()[-1200:]
        if model is None:
            raise ExternalServiceError("未启用可用于生成文本的模型", provider="model")
        rag_result = RagResult(question=prompt, knowledge_scopes=[], chunks=[], prompt=prompt, allow_general_knowledge=True)
        return _call_configured_model(model, rag_result, allow_no_hit=True)

    @staticmethod
    def generate_text_for_scenario(
        db: Session,
        prompt: str,
        scenario: ModelScenario,
    ) -> ModelCompletion:
        route = ModelRoutingService.resolve(db, scenario)
        model = route.model
        if _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled):
            answer = prompt.strip()[-1200:]
            model_name = model.model_name if model is not None else "mock-model"
        else:
            if model is None:
                raise ExternalServiceError(
                    f"未启用可用于{scenario}场景的模型",
                    provider="model",
                )
            rag_result = RagResult(
                question=prompt,
                knowledge_scopes=[],
                chunks=[],
                prompt=prompt,
                allow_general_knowledge=True,
            )
            answer = _call_configured_model(model, rag_result, allow_no_hit=True)
            model_name = model.model_name
        return ModelCompletion(
            answer=answer,
            model_id=model.id if model is not None else None,
            model_name=model_name,
            input_token=_rough_token_count(prompt),
            output_token=_rough_token_count(answer),
            route_reason=route.reason,
        )

    @staticmethod
    async def summarize_or_raise_async(
        db: Session,
        messages_text: str,
        previous_summary: str | None = None,
    ) -> str:
        if _system_config_bool(db, "mock_model_enabled", get_settings().mock_model_enabled):
            combined = "\n".join(part for part in [previous_summary, messages_text] if part).strip()
            return combined[-1000:]
        model = ModelClientService._get_enabled_model(db)
        if model is None:
            raise ExternalServiceError("未启用可用于生成历史摘要的模型", provider="model")
        return await asyncio.to_thread(_generate_summary, model, messages_text, previous_summary)

    @staticmethod
    def _get_enabled_model(db: Session) -> ModelConfig | None:
        return ModelRoutingService.default_model(db)

    @staticmethod
    def test_model(model: ModelConfig) -> dict[str, Any]:
        test_prompt = "请回复 OK，用于测试模型配置是否可用。"
        rag_result = RagResult(question=test_prompt, knowledge_scopes=[], chunks=[], prompt=test_prompt)
        try:
            answer = _call_configured_model(model, rag_result, allow_no_hit=True)
        except ExternalServiceError as exc:
            return {"ok": False, "message": str(exc), "answer": ""}
        return {"ok": True, "message": "模型配置可用", "answer": answer[:500]}

    @staticmethod
    def debug_model(model: ModelConfig, rag_result: RagResult, overrides: dict[str, Any]) -> dict[str, Any]:
        debug_model = _copy_model_with_overrides(model, overrides)
        try:
            answer = _call_configured_model(debug_model, rag_result, allow_no_hit=True)
        except ExternalServiceError as exc:
            return {"ok": False, "message": str(exc), "answer": "", "prompt": rag_result.prompt}
        return {
            "ok": True,
            "message": "Agent 调试完成",
            "answer": answer,
            "prompt": rag_result.prompt,
            "modelName": model.model_name,
            "retrieveCount": len(rag_result.chunks),
            "knowledgeIds": rag_result.knowledge_ids,
        }

    @staticmethod
    async def debug_model_async(model: ModelConfig, rag_result: RagResult, overrides: dict[str, Any]) -> dict[str, Any]:
        """Run the blocking provider client outside the ASGI event loop."""
        return await asyncio.to_thread(ModelClientService.debug_model, model, rag_result, overrides)


def _mock_answer(rag_result: RagResult) -> str:
    if not rag_result.is_hit:
        return NO_HIT_ANSWER

    summaries = "\n".join(
        f"- {chunk.content}\n  来源：{chunk.knowledge_name} / {chunk.title}" for chunk in rag_result.chunks
    )
    return (
        "根据当前已开放知识库，整理到的信息如下：\n\n"
        f"{summaries}\n\n"
        "当前使用模型 mock 生成回答，知识片段来自搜索接口或飞书直读检索链路；"
        "关闭模型 mock 后会使用后台启用的大模型配置。"
    )


def _annotate_model_error(
    error: ExternalServiceError,
    model: ModelConfig,
    route_reason: str,
    question_type: str | None,
) -> None:
    error.model_id = model.id
    error.model_name = model.model_name
    error.route_reason = route_reason
    error.question_type = question_type


def _system_config_bool(db: Session, key: str, default: bool) -> bool:
    config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == key))
    if config is None or not config.config_value.strip():
        return default
    return config.config_value.strip().lower() in {"1", "true", "yes", "on", "启用"}


def _copy_model_with_overrides(model: ModelConfig, overrides: dict[str, Any]) -> ModelConfig:
    debug_model = ModelConfig(
        provider=model.provider,
        display_name=model.display_name,
        api_type=model.api_type,
        model_name=model.model_name,
        base_url=model.base_url,
        api_url=model.api_url,
        api_key=model.api_key,
        auth_type=model.auth_type,
        api_version=model.api_version,
        temperature=overrides.get("temperature"),
        top_p=overrides.get("top_p"),
        top_k=overrides.get("top_k"),
        presence_penalty=overrides.get("presence_penalty"),
        frequency_penalty=overrides.get("frequency_penalty"),
        max_token=overrides.get("max_token"),
        context_window=model.context_window,
        stream_enabled=overrides.get("stream_enabled", model.stream_enabled),
        response_format=model.response_format,
        extra_params=model.extra_params,
        remark=model.remark,
        timeout_second=model.timeout_second,
        enabled=model.enabled,
    )
    debug_model.id = model.id
    return debug_model


def _rough_token_count(text: str) -> int:
    return max(1, len(text.strip()) // 2)


def _call_configured_model(model: ModelConfig, rag_result: RagResult, *, allow_no_hit: bool = False) -> str:
    if not allow_no_hit and not rag_result.is_hit:
        return NO_HIT_ANSWER
    if not (model.api_url or model.base_url) or not model.api_key:
        raise ExternalServiceError("模型 Base URL/API URL 或 API Key 未配置", provider="model")

    api_type = model.api_type or "openai_compatible"
    if api_type == "anthropic_messages":
        return _call_anthropic_messages(model, rag_result)
    if api_type == "gemini_generate_content":
        return _call_gemini_generate_content(model, rag_result)
    if api_type == "minimax":
        return _call_minimax(model, rag_result)
    return _call_openai_compatible_model(model, rag_result)


def _generate_summary(
    model: ModelConfig,
    messages_text: str,
    previous_summary: str | None = None,
) -> str:
    """用模型把历史对话压缩成摘要"""
    summary_prompt = (
        "请把已有摘要和新增历史滚动重写成一份完整、无重复的会话记忆。"
        "必须覆盖：用户背景与明确事实、用户当前目标、已确认结论、用户偏好和限制、"
        "尚未解决的问题、最近对话进展。后出现的修正覆盖旧信息。"
        "只输出中文摘要，不要解释，控制在1000字以内。\n\n"
        f"已有摘要：\n{previous_summary or '无'}\n\n"
        f"新增历史：\n{messages_text}"
    )
    rag_result = RagResult(
        question=summary_prompt,
        knowledge_scopes=[],
        chunks=[],
        prompt=summary_prompt,
    )
    return _call_configured_model(model, rag_result, allow_no_hit=True)


def _call_openai_compatible_model(model: ModelConfig, rag_result: RagResult) -> str:
    payload = {
        "model": model.model_name,
        "messages": _openai_messages(rag_result, model),
        "stream": False,
        "max_tokens": _max_output_tokens(model),
    }
    _put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
    _put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))
    _put_if_not_none(payload, "presence_penalty", _decimal_to_float(model.presence_penalty))
    _put_if_not_none(payload, "frequency_penalty", _decimal_to_float(model.frequency_penalty))
    if model.response_format == "json_object":
        payload["response_format"] = {"type": "json_object"}
    payload.update(_load_extra_params(model.extra_params))

    headers = _auth_headers(model)
    try:
        response = httpx.post(
            _resolve_openai_endpoint(model),
            json=payload,
            headers=headers,
            timeout=model.timeout_second,
        )
        response.raise_for_status()
        data = response.json()
        return _extract_openai_answer(data)
    except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
        raise ExternalServiceError(f"模型调用失败：{exc}", provider="model") from exc


def _call_anthropic_messages(model: ModelConfig, rag_result: RagResult) -> str:
    system, messages = _system_and_turn_messages(rag_result, model)
    payload = {
        "model": model.model_name,
        "max_tokens": _max_output_tokens(model),
        "system": system,
        "messages": messages,
        "stream": False,
    }
    _put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
    _put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))
    _put_if_not_none(payload, "top_k", model.top_k)
    payload.update(_load_extra_params(model.extra_params))

    headers = _anthropic_headers(model)
    try:
        response = httpx.post(
            _resolve_anthropic_endpoint(model),
            json=payload,
            headers=headers,
            timeout=model.timeout_second,
        )
        response.raise_for_status()
        return _extract_anthropic_answer(response.json())
    except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
        raise ExternalServiceError(f"模型调用失败：{exc}", provider="model") from exc


def _call_gemini_generate_content(model: ModelConfig, rag_result: RagResult) -> str:
    generation_config: dict[str, Any] = {}
    _put_if_not_none(generation_config, "temperature", _decimal_to_float(model.temperature))
    _put_if_not_none(generation_config, "topP", _decimal_to_float(model.top_p))
    _put_if_not_none(generation_config, "topK", model.top_k)
    generation_config["maxOutputTokens"] = _max_output_tokens(model)
    if model.response_format == "json_object":
        generation_config["responseMimeType"] = "application/json"

    system, messages = _system_and_turn_messages(rag_result, model)
    payload = {
        "systemInstruction": {"parts": [{"text": system}]},
        "contents": [
            {
                "role": "model" if message["role"] == "assistant" else "user",
                "parts": [{"text": message["content"]}],
            }
            for message in messages
        ],
    }
    if generation_config:
        payload["generationConfig"] = generation_config
    payload.update(_load_extra_params(model.extra_params))

    try:
        response = httpx.post(
            _resolve_gemini_endpoint(model),
            json=payload,
            headers={"Content-Type": "application/json"},
            timeout=model.timeout_second,
        )
        response.raise_for_status()
        data = response.json()
        # Gemini 有时返回 HTTP 200 但 body 里有 error
        if "error" in data:
            err = data["error"]
            msg = err.get("message", str(err))
            raise ExternalServiceError(f"Gemini 调用失败：{msg}", provider="model")
        return _extract_gemini_answer(data)
    except ExternalServiceError:
        raise
    except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
        raise ExternalServiceError(f"模型调用失败：{exc}", provider="model") from exc


def _extract_openai_answer(data: dict[str, Any]) -> str:
    choices = data.get("choices")
    if not isinstance(choices, list) or not choices:
        raise ValueError("模型响应缺少 choices")

    first_choice = choices[0]
    if not isinstance(first_choice, dict):
        raise ValueError("模型响应 choices 格式不正确")

    message = first_choice.get("message")
    if isinstance(message, dict) and message.get("content"):
        return str(message["content"])

    if first_choice.get("text"):
        return str(first_choice["text"])

    raise ValueError("模型响应缺少回答内容")


def _extract_anthropic_answer(data: dict[str, Any]) -> str:
    content = data.get("content")
    if not isinstance(content, list):
        raise ValueError("Anthropic 响应缺少 content")
    texts = [item.get("text", "") for item in content if isinstance(item, dict) and item.get("type") == "text"]
    answer = "".join(texts).strip()
    if not answer:
        raise ValueError("Anthropic 响应缺少文本内容")
    return answer


def _call_minimax(model: ModelConfig, rag_result: RagResult) -> str:
    """调用 MiniMax 官方 API (非 OpenAI 兼容协议)"""
    import urllib.parse

    extra = _load_extra_params(model.extra_params)
    group_id = extra.pop("group_id", "")
    if not group_id:
        raise ExternalServiceError("MiniMax 调用需要 group_id，请在模型高级参数中配置：{\"group_id\": \"xxx\"}", provider="model")

    base_url = (model.base_url or "https://api.minimaxi.com/v1/text/chatcompletion_pro").rstrip("/")
    # MiniMax 的 baseUrl 在快速添加模板里已经包含完整路径，但如果用户自定义 baseUrl 没有 query，需要拼接
    if "?" not in base_url:
        base_url = f"{base_url}?GroupId={urllib.parse.quote(group_id)}"
    elif "GroupId" not in base_url:
        base_url = f"{base_url}&GroupId={urllib.parse.quote(group_id)}"

    system, messages = _system_and_turn_messages(rag_result, model)
    payload: dict[str, Any] = {
        "model": model.model_name,
        "tokens_to_generate": _max_output_tokens(model),
        "reply_constraints": {"sender_type": "BOT", "sender_name": "大本营答疑助手"},
        "messages": [
            {
                "sender_type": "BOT" if message["role"] == "assistant" else "USER",
                "sender_name": "大本营答疑助手" if message["role"] == "assistant" else "用户",
                "text": message["content"],
            }
            for message in messages
        ],
        "bot_setting": [
            {
                "bot_name": "大本营答疑助手",
                "content": system,
            }
        ],
    }
    _put_if_not_none(payload, "temperature", _decimal_to_float(model.temperature))
    _put_if_not_none(payload, "top_p", _decimal_to_float(model.top_p))

    headers = {"Authorization": f"Bearer {_model_api_key(model)}", "Content-Type": "application/json"}

    try:
        response = httpx.post(base_url, json=payload, headers=headers, timeout=model.timeout_second)
        response.raise_for_status()
        return _extract_minimax_answer(response.json())
    except (httpx.HTTPError, ValueError, KeyError, TypeError) as exc:
        raise ExternalServiceError(f"MiniMax 模型调用失败：{exc}", provider="model") from exc


def _extract_minimax_answer(data: dict[str, Any]) -> str:
    choices = data.get("choices")
    if not isinstance(choices, list) or not choices:
        raise ValueError("MiniMax 响应缺少 choices")
    first = choices[0]
    messages = first.get("messages", [])
    if not isinstance(messages, list):
        raise ValueError("MiniMax 响应缺少 messages")
    texts = [m.get("text", "") for m in messages if isinstance(m, dict)]
    answer = "".join(texts).strip()
    if not answer:
        raise ValueError("MiniMax 响应缺少回答内容")
    return answer


def _extract_gemini_answer(data: dict[str, Any]) -> str:
    candidates = data.get("candidates")
    if not isinstance(candidates, list) or not candidates:
        raise ValueError("Gemini 响应缺少 candidates")
    parts = candidates[0].get("content", {}).get("parts", [])
    texts = [item.get("text", "") for item in parts if isinstance(item, dict)]
    answer = "".join(texts).strip()
    if not answer:
        raise ValueError("Gemini 响应缺少文本内容")
    return answer


def _resolve_openai_endpoint(model: ModelConfig) -> str:
    if model.api_url:
        return model.api_url
    base_url = (model.base_url or "").rstrip("/")
    return f"{base_url}/chat/completions"


def _resolve_anthropic_endpoint(model: ModelConfig) -> str:
    if model.api_url:
        return model.api_url
    base_url = (model.base_url or "https://api.anthropic.com").rstrip("/")
    return f"{base_url}/v1/messages"


def _resolve_gemini_endpoint(model: ModelConfig) -> str:
    if model.api_url:
        return model.api_url
    base_url = (model.base_url or "https://generativelanguage.googleapis.com/v1beta").rstrip("/")
    query = urlencode({"key": _model_api_key(model)})
    return f"{base_url}/models/{model.model_name}:generateContent?{query}"


def _auth_headers(model: ModelConfig) -> dict[str, str]:
    headers = {"Content-Type": "application/json"}
    if model.auth_type == "api_key":
        headers["x-api-key"] = _model_api_key(model)
    else:
        headers["Authorization"] = f"Bearer {_model_api_key(model)}"
    if model.api_version:
        headers["api-version"] = model.api_version
    return headers


def _anthropic_headers(model: ModelConfig) -> dict[str, str]:
    headers = {"Content-Type": "application/json"}
    if model.auth_type == "bearer":
        headers["Authorization"] = f"Bearer {_model_api_key(model)}"
    else:
        headers["x-api-key"] = _model_api_key(model)
    if model.api_version:
        headers["anthropic-version"] = model.api_version
    elif model.provider == "anthropic":
        headers["anthropic-version"] = "2023-06-01"
    return headers


def _decimal_to_float(value: Any) -> float | None:
    return float(value) if value is not None else None


def _model_api_key(model: ModelConfig) -> str:
    return SecretService.decrypt(model.api_key)


def _agent_system_prompt() -> str:
    return (
        "你是大本营答疑助手。必须遵守最低安全规则。涉及课程、老师观点和公司业务时只能依据提供的正式知识，"
        "没有可靠知识必须明确依据不足；普通常识可以谨慎回答。不得编造老师观点，不得输出整篇课程资料或大段连续原文。"
    )


def _max_output_tokens(model: ModelConfig) -> int:
    configured = max(1, model.max_token or 8192)
    if not model.context_window:
        return configured
    return min(configured, max(1, model.context_window // 3))


def _openai_messages(
    rag_result: RagResult,
    model: ModelConfig | None = None,
) -> list[dict[str, str]]:
    system, messages = _system_and_turn_messages(rag_result, model)
    return [{"role": "system", "content": system}, *messages]


def _system_and_turn_messages(
    rag_result: RagResult,
    model: ModelConfig | None = None,
) -> tuple[str, list[dict[str, str]]]:
    source = _fit_source_messages(rag_result, model)
    system_parts = [message["content"] for message in source if message.get("role") == "system"]
    system = "\n\n".join(system_parts).strip() or _agent_system_prompt()
    turns: list[dict[str, str]] = []
    for message in source:
        role = message.get("role")
        content = str(message.get("content", "")).strip()
        if role not in {"user", "assistant"} or not content:
            continue
        if turns and turns[-1]["role"] == role:
            turns[-1]["content"] += "\n\n" + content
        else:
            turns.append({"role": role, "content": content})
    if turns and turns[0]["role"] == "assistant":
        system += "\n\n[最近一条助手历史回复]\n" + turns.pop(0)["content"]
    if not turns:
        turns.append({"role": "user", "content": rag_result.prompt})
    return system, turns


def _fit_source_messages(rag_result: RagResult, model: ModelConfig | None) -> list[dict[str, str]]:
    fallback_messages = [
        {"role": "system", "content": _agent_system_prompt()},
        {"role": "user", "content": rag_result.prompt},
    ]
    source = [dict(message) for message in (rag_result.messages or fallback_messages)]
    if model is None or not model.context_window:
        return source

    output_budget = _max_output_tokens(model)
    input_budget = max(1, model.context_window - output_budget - 64)

    def token_count() -> int:
        return sum(_rough_token_count(str(message.get("content", ""))) for message in source)

    while token_count() > input_budget:
        removable = next(
            (
                index
                for index, message in enumerate(source[:-1])
                if message.get("role") in {"user", "assistant"}
            ),
            None,
        )
        if removable is None:
            break
        source.pop(removable)

    while token_count() > input_budget:
        system_indexes = [index for index, message in enumerate(source) if message.get("role") == "system"]
        if not system_indexes:
            break
        longest = max(system_indexes, key=lambda index: len(str(source[index].get("content", ""))))
        content = str(source[longest].get("content", ""))
        excess_chars = max(100, (token_count() - input_budget) * 2)
        target_length = max(200, len(content) - excess_chars)
        if target_length >= len(content):
            break
        half = target_length // 2
        source[longest]["content"] = content[:half] + "\n...[上下文窗口自动截断]...\n" + content[-half:]
    return source


def _put_if_not_none(target: dict[str, Any], key: str, value: Any) -> None:
    if value is not None:
        target[key] = value


def _load_extra_params(extra_params: str | None) -> dict[str, Any]:
    if not extra_params:
        return {}
    try:
        data = json.loads(extra_params)
    except json.JSONDecodeError as exc:
        raise ExternalServiceError(f"模型高级参数不是合法 JSON：{exc}", provider="model") from exc
    if not isinstance(data, dict):
        raise ExternalServiceError("模型高级参数必须是 JSON 对象", provider="model")
    return data
