from __future__ import annotations

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
from app.services.external_errors import ExternalServiceError
from app.services.rag_service import NO_HIT_ANSWER, RagResult


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


class ModelClientService:
    @staticmethod
    def complete(db: Session, rag_result: RagResult) -> ModelCompletion:
        model = db.scalar(
            select(ModelConfig)
            .where(ModelConfig.enabled == 1)
            .order_by(ModelConfig.id.desc())
            .limit(1)
        )
        settings = get_settings()
        if settings.mock_model_enabled or model is None:
            model_name = model.model_name if model is not None else "mock-model"
            answer = _mock_answer(rag_result)
        else:
            model_name = model.model_name
            answer = _call_configured_model(model, rag_result)

        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),
        )

    @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]}


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 RAG + mock 模型阶段，后续会把检索服务替换为飞书实时检索，"
        "把模型服务替换为后台启用的大模型配置。"
    )


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)
    return _call_openai_compatible_model(model, rag_result)


def _call_openai_compatible_model(model: ModelConfig, rag_result: RagResult) -> str:
    payload = {
        "model": model.model_name,
        "messages": [
            {"role": "system", "content": "你是企业知识库问答助手，只能基于已提供的知识片段回答。"},
            {"role": "user", "content": rag_result.prompt},
        ],
        "stream": False,
        "max_tokens": model.max_token or 1024,
    }
    _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()
        return _extract_openai_answer(response.json())
    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:
    payload = {
        "model": model.model_name,
        "max_tokens": model.max_token or 1024,
        "system": "你是企业知识库问答助手，只能基于已提供的知识片段回答。",
        "messages": [{"role": "user", "content": rag_result.prompt}],
        "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 = {
        "x-api-key": model.api_key,
        "anthropic-version": model.api_version or "2023-06-01",
        "Content-Type": "application/json",
    }
    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)
    _put_if_not_none(generation_config, "maxOutputTokens", model.max_token)
    if model.response_format == "json_object":
        generation_config["responseMimeType"] = "application/json"

    payload = {
        "systemInstruction": {
            "parts": [{"text": "你是企业知识库问答助手，只能基于已提供的知识片段回答。"}]
        },
        "contents": [{"role": "user", "parts": [{"text": rag_result.prompt}]}],
    }
    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()
        return _extract_gemini_answer(response.json())
    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 _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})
    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
    else:
        headers["Authorization"] = f"Bearer {model.api_key}"
    if model.api_version:
        headers["api-version"] = model.api_version
    return headers


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


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
