from __future__ import annotations

import json
from collections.abc import Callable
from http import HTTPStatus
from typing import Any

from app.services.model_connectivity import (
    AZURE_API_VERSION,
    _build_azure_deployment_base,
    _build_headers,
    _ensure_path,
    _normalize_endpoint,
)
from app.services.runtime_chat_attempts import (
    RuntimeChatAuthoritativeUsage,
    RuntimeChatProviderAttemptError,
    RuntimeChatProviderResponse,
    RuntimeChatToolCall,
)

SendJsonRequest = Callable[..., tuple[int, Any]]


def request_openai_compatible_completion(
    *,
    send_json_request: SendJsonRequest,
    provider: str,
    endpoint: str,
    model: str,
    api_key: str,
    messages: list[dict[str, Any]],
    max_tokens: int,
    temperature: float,
    timeout_seconds: int,
) -> RuntimeChatProviderResponse[str]:
    url = _ensure_path(_normalize_endpoint(endpoint), "chat/completions")
    request_payload: dict[str, Any] = {
        "model": model,
        "messages": messages,
        "max_tokens": max_tokens,
        "temperature": temperature,
    }
    if provider == "GLM":
        request_payload["thinking"] = {"type": "disabled"}
    payload = _send_and_require_success(
        send_json_request=send_json_request,
        provider=provider,
        method="POST",
        url=url,
        headers=_build_headers(api_key=api_key, use_bearer=True),
        request_payload=request_payload,
        timeout_seconds=timeout_seconds,
        error_prefix="模型接口",
    )
    response_id, response_model, usage = extract_response_metadata(provider, payload)
    try:
        output = extract_openai_text(payload)
    except Exception as exc:
        raise _postprocess_error(
            exc,
            response_id=response_id,
            response_model=response_model,
            usage=usage,
        ) from exc
    return RuntimeChatProviderResponse(
        output=output,
        response_id=response_id,
        response_model=response_model,
        usage=usage,
    )


def request_openai_compatible_tool_call(
    *,
    send_json_request: SendJsonRequest,
    provider: str,
    endpoint: str,
    model: str,
    api_key: str,
    messages: list[dict[str, Any]],
    tools: list[dict[str, Any]],
    tool_choice: dict[str, Any] | str | None,
    max_tokens: int,
    temperature: float,
    timeout_seconds: int,
) -> RuntimeChatProviderResponse[RuntimeChatToolCall | None]:
    url = _ensure_path(_normalize_endpoint(endpoint), "chat/completions")
    request_payload: dict[str, Any] = {
        "model": model,
        "messages": messages,
        "tools": tools,
        "tool_choice": tool_choice or "auto",
        "max_tokens": max_tokens,
        "temperature": temperature,
    }
    # function calling 的强制 tool_choice 与部分供应商 thinking mode 冲突。
    if provider in {"GLM", "Ali"}:
        request_payload["thinking"] = {"type": "disabled"}
    payload = _send_and_require_success(
        send_json_request=send_json_request,
        provider=provider,
        method="POST",
        url=url,
        headers=_build_headers(api_key=api_key, use_bearer=True),
        request_payload=request_payload,
        timeout_seconds=timeout_seconds,
        error_prefix="模型接口",
    )
    return _build_tool_response(provider, payload)


def request_ollama_completion(
    *,
    send_json_request: SendJsonRequest,
    endpoint: str,
    model: str,
    api_key: str,
    messages: list[dict[str, Any]],
    max_tokens: int,
    temperature: float,
    timeout_seconds: int,
) -> RuntimeChatProviderResponse[str]:
    provider = "Ollama"
    url = _ensure_path(_normalize_endpoint(endpoint), "api/chat")
    payload = _send_and_require_success(
        send_json_request=send_json_request,
        provider=provider,
        method="POST",
        url=url,
        headers=_build_headers(api_key=api_key, use_bearer=False),
        request_payload={
            "model": model,
            "messages": messages,
            "stream": False,
            "options": {
                "num_predict": max_tokens,
                "temperature": temperature,
            },
        },
        timeout_seconds=timeout_seconds,
        error_prefix="Ollama",
    )
    response_id, response_model, usage = extract_response_metadata(provider, payload)
    try:
        message = payload.get("message") if isinstance(payload, dict) else None
        output = str(message.get("content") or "").strip() if isinstance(message, dict) else ""
    except Exception as exc:
        raise _postprocess_error(
            exc,
            response_id=response_id,
            response_model=response_model,
            usage=usage,
        ) from exc
    return RuntimeChatProviderResponse(
        output=output,
        response_id=response_id,
        response_model=response_model,
        usage=usage,
    )


def request_azure_openai_completion(
    *,
    send_json_request: SendJsonRequest,
    endpoint: str,
    model: str,
    api_key: str,
    messages: list[dict[str, Any]],
    max_tokens: int,
    temperature: float,
    timeout_seconds: int,
) -> RuntimeChatProviderResponse[str]:
    provider = "Azure OpenAI"
    deployment_base = _build_azure_deployment_base(endpoint, model)
    url = f"{deployment_base}/chat/completions?api-version={AZURE_API_VERSION}"
    payload = _send_and_require_success(
        send_json_request=send_json_request,
        provider=provider,
        method="POST",
        url=url,
        headers=_build_headers(api_key=api_key, use_bearer=False, use_api_key=True),
        request_payload={
            "messages": messages,
            "max_tokens": max_tokens,
            "temperature": temperature,
        },
        timeout_seconds=timeout_seconds,
        error_prefix=provider,
    )
    response_id, response_model, usage = extract_response_metadata(provider, payload)
    try:
        output = extract_openai_text(payload)
    except Exception as exc:
        raise _postprocess_error(
            exc,
            response_id=response_id,
            response_model=response_model,
            usage=usage,
        ) from exc
    return RuntimeChatProviderResponse(
        output=output,
        response_id=response_id,
        response_model=response_model,
        usage=usage,
    )


def request_azure_openai_tool_call(
    *,
    send_json_request: SendJsonRequest,
    endpoint: str,
    model: str,
    api_key: str,
    messages: list[dict[str, Any]],
    tools: list[dict[str, Any]],
    tool_choice: dict[str, Any] | str | None,
    max_tokens: int,
    temperature: float,
    timeout_seconds: int,
) -> RuntimeChatProviderResponse[RuntimeChatToolCall | None]:
    provider = "Azure OpenAI"
    deployment_base = _build_azure_deployment_base(endpoint, model)
    url = f"{deployment_base}/chat/completions?api-version={AZURE_API_VERSION}"
    payload = _send_and_require_success(
        send_json_request=send_json_request,
        provider=provider,
        method="POST",
        url=url,
        headers=_build_headers(api_key=api_key, use_bearer=False, use_api_key=True),
        request_payload={
            "messages": messages,
            "tools": tools,
            "tool_choice": tool_choice or "auto",
            "max_tokens": max_tokens,
            "temperature": temperature,
        },
        timeout_seconds=timeout_seconds,
        error_prefix=provider,
    )
    return _build_tool_response(provider, payload)


def extract_response_metadata(
    provider: str,
    payload: Any,
) -> tuple[str | None, str | None, RuntimeChatAuthoritativeUsage]:
    if not isinstance(payload, dict):
        return None, None, RuntimeChatAuthoritativeUsage()
    response_id = _non_empty_string(payload.get("id"))
    response_model = _non_empty_string(payload.get("model"))
    return response_id, response_model, extract_authoritative_usage(provider, payload)


def extract_authoritative_usage(
    provider: str,
    payload: Any,
) -> RuntimeChatAuthoritativeUsage:
    if not isinstance(payload, dict):
        return RuntimeChatAuthoritativeUsage()
    if provider == "Ollama":
        prompt_eval_count = _authoritative_non_negative_int(
            payload.get("prompt_eval_count")
        )
        eval_count = _authoritative_non_negative_int(payload.get("eval_count"))
        has_usage = prompt_eval_count is not None or eval_count is not None
        return RuntimeChatAuthoritativeUsage(
            source="ollama_counts" if has_usage else "unavailable",
            availability=_availability((prompt_eval_count, eval_count)),
            prompt_eval_count=prompt_eval_count,
            eval_count=eval_count,
        )

    usage = payload.get("usage")
    if not isinstance(usage, dict):
        return RuntimeChatAuthoritativeUsage()
    prompt_tokens = _authoritative_non_negative_int(usage.get("prompt_tokens"))
    completion_tokens = _authoritative_non_negative_int(
        usage.get("completion_tokens")
    )
    total_tokens = _authoritative_non_negative_int(usage.get("total_tokens"))
    values = (prompt_tokens, completion_tokens, total_tokens)
    return RuntimeChatAuthoritativeUsage(
        source="openai_usage" if any(value is not None for value in values) else "unavailable",
        availability=_availability(values),
        prompt_tokens=prompt_tokens,
        completion_tokens=completion_tokens,
        total_tokens=total_tokens,
    )


def extract_openai_text(payload: Any) -> str:
    if not isinstance(payload, dict):
        return ""
    choices = payload.get("choices")
    if not isinstance(choices, list) or not choices:
        return ""
    first_choice = choices[0]
    if not isinstance(first_choice, dict):
        return ""
    message = first_choice.get("message")
    if isinstance(message, dict):
        content = message.get("content", "")
        if isinstance(content, str):
            return content.strip()
        if isinstance(content, list):
            parts = [
                str(item.get("text") or "").strip()
                for item in content
                if isinstance(item, dict) and item.get("type") == "text"
            ]
            return "\n".join(part for part in parts if part).strip()
    text = first_choice.get("text")
    return text.strip() if isinstance(text, str) else ""


def extract_openai_tool_call(payload: Any) -> RuntimeChatToolCall | None:
    if not isinstance(payload, dict):
        return None
    choices = payload.get("choices")
    if not isinstance(choices, list) or not choices:
        return None
    first_choice = choices[0]
    if not isinstance(first_choice, dict):
        return None
    message = first_choice.get("message")
    if not isinstance(message, dict):
        return None
    tool_calls = message.get("tool_calls")
    if isinstance(tool_calls, list) and tool_calls:
        first_tool = tool_calls[0]
        if isinstance(first_tool, dict):
            function_payload = first_tool.get("function")
            if isinstance(function_payload, dict):
                return build_runtime_tool_call(
                    name=function_payload.get("name"),
                    arguments=function_payload.get("arguments"),
                    call_id=first_tool.get("id"),
                )
    function_call = message.get("function_call")
    if isinstance(function_call, dict):
        return build_runtime_tool_call(
            name=function_call.get("name"),
            arguments=function_call.get("arguments"),
            call_id=None,
        )
    return None


def build_runtime_tool_call(
    *,
    name: Any,
    arguments: Any,
    call_id: Any,
) -> RuntimeChatToolCall | None:
    tool_name = str(name or "").strip()
    if not tool_name:
        return None
    if isinstance(arguments, dict):
        parsed_arguments = arguments
        raw_arguments = json.dumps(arguments, ensure_ascii=False)
    else:
        raw_arguments = str(arguments or "").strip()
        if not raw_arguments:
            parsed_arguments = {}
        else:
            parsed = json.loads(raw_arguments)
            if not isinstance(parsed, dict):
                raise ValueError("工具调用参数必须是 JSON object。")
            parsed_arguments = parsed
    return RuntimeChatToolCall(
        name=tool_name,
        arguments=parsed_arguments,
        call_id=str(call_id).strip() if call_id else None,
        raw_arguments=raw_arguments,
    )


def _send_and_require_success(
    *,
    send_json_request: SendJsonRequest,
    provider: str,
    method: str,
    url: str,
    headers: dict[str, str],
    request_payload: dict[str, Any],
    timeout_seconds: int,
    error_prefix: str,
) -> Any:
    try:
        status_code, payload = send_json_request(
            method,
            url,
            headers=headers,
            payload=request_payload,
            timeout_seconds=timeout_seconds,
        )
    except Exception as exc:
        status_code = _provider_status_code(exc)
        if status_code is not None and status_code >= HTTPStatus.BAD_REQUEST:
            raise RuntimeChatProviderAttemptError(
                str(exc) or f"{error_prefix}返回异常状态 {status_code}。",
                outcome="provider_rejected",
                status_code=status_code,
            ) from exc
        raise RuntimeChatProviderAttemptError(
            str(exc) or f"{error_prefix}请求结果未知。",
            outcome="outcome_unknown",
        ) from exc
    if isinstance(status_code, bool) or not isinstance(status_code, int):
        raise RuntimeChatProviderAttemptError(
            f"{error_prefix}返回了无效 HTTP 状态码。",
            outcome="outcome_unknown",
        )
    if status_code < HTTPStatus.BAD_REQUEST:
        return payload
    response_id, response_model, usage = extract_response_metadata(provider, payload)
    raise RuntimeChatProviderAttemptError(
        f"{error_prefix}返回异常状态 {status_code}。",
        outcome="provider_rejected",
        response_id=response_id,
        response_model=response_model,
        usage=usage,
        status_code=status_code,
    )


def _build_tool_response(
    provider: str,
    payload: Any,
) -> RuntimeChatProviderResponse[RuntimeChatToolCall | None]:
    response_id, response_model, usage = extract_response_metadata(provider, payload)
    try:
        output = extract_openai_tool_call(payload)
    except Exception as exc:
        raise _postprocess_error(
            exc,
            response_id=response_id,
            response_model=response_model,
            usage=usage,
        ) from exc
    return RuntimeChatProviderResponse(
        output=output,
        response_id=response_id,
        response_model=response_model,
        usage=usage,
    )


def _postprocess_error(
    exc: Exception,
    *,
    response_id: str | None,
    response_model: str | None,
    usage: RuntimeChatAuthoritativeUsage,
) -> RuntimeChatProviderAttemptError:
    return RuntimeChatProviderAttemptError(
        str(exc) or "模型响应后处理失败。",
        outcome="postprocess_failed",
        response_id=response_id,
        response_model=response_model,
        usage=usage,
    )


def _authoritative_non_negative_int(value: Any) -> int | None:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        return None
    return value


def _availability(values: tuple[int | None, ...]) -> str:
    available_count = sum(value is not None for value in values)
    if available_count == 0:
        return "unavailable"
    if available_count == len(values):
        return "available"
    return "partial"


def _non_empty_string(value: Any) -> str | None:
    normalized = str(value or "").strip()
    return normalized or None


def _provider_status_code(exc: Exception) -> int | None:
    value = getattr(exc, "status_code", None)
    if isinstance(value, bool) or not isinstance(value, int):
        return None
    return value
