"""Gemini Provider - 原生 Google Gemini API 支持"""

import json
import logging
import time
from typing import Any

import httpx

from agentkit.core.exceptions import LLMProviderError
from agentkit.llm.protocol import (
    LLMProvider,
    LLMRequest,
    LLMResponse,
    StreamChunk,
    TokenUsage,
    ToolCall,
)
from agentkit.llm.retry import (
    CircuitBreaker,
    CircuitBreakerConfig,
    RetryConfig,
    RetryPolicy,
)

logger = logging.getLogger(__name__)


class _GeminiStreamContext:
    """Wraps an httpx streaming response context manager for use with retry/circuit breaker."""

    def __init__(self, response_ctx, response):
        self._response_ctx = response_ctx
        self._response = response

    async def __aenter__(self):
        return self._response

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        return await self._response_ctx.__aexit__(exc_type, exc_val, exc_tb)


class GeminiProvider(LLMProvider):
    """Google Gemini API 原生 Provider"""

    def __init__(
        self,
        api_key: str,
        model: str = "gemini-2.0-flash",
        max_output_tokens: int = 4096,
        base_url: str = "https://generativelanguage.googleapis.com",
        timeout: float = 120.0,
        safety_settings: list | None = None,
        retry_config: RetryConfig | None = None,
        circuit_breaker_config: CircuitBreakerConfig | None = None,
    ):
        self._api_key = api_key
        self._model = model
        self._max_output_tokens = max_output_tokens
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._safety_settings = safety_settings
        self._client: httpx.AsyncClient | None = None
        self._retry_policy = RetryPolicy(retry_config) if retry_config else None
        self._circuit_breaker = (
            CircuitBreaker(circuit_breaker_config, provider="gemini")
            if circuit_breaker_config
            else None
        )

    def _get_client(self) -> httpx.AsyncClient:
        """Lazy client initialization"""
        if self._client is None:
            self._client = httpx.AsyncClient(timeout=self._timeout)
        return self._client

    async def close(self) -> None:
        """关闭 HTTP 客户端连接池"""
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    def _convert_messages(
        self, messages: list[dict[str, str]]
    ) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
        """将 OpenAI 风格消息转换为 Gemini 格式

        Returns:
            (system_instruction, contents)
        """
        system_instruction: dict[str, Any] | None = None
        contents: list[dict[str, Any]] = []

        for msg in messages:
            role = msg.get("role", "")
            content = msg.get("content", "")

            if role == "system":
                system_instruction = {"parts": [{"text": content}]}
                continue

            if role == "user":
                # Check if this is a tool result message
                if msg.get("tool_call_id"):
                    # Tool response: role="user" with functionResponse part
                    tool_name = msg.get("name", "")
                    # If name not at top level, try to extract from content
                    if not tool_name and isinstance(content, str):
                        try:
                            parsed = json.loads(content)
                            tool_name = parsed.get("name", "")
                        except (json.JSONDecodeError, AttributeError):
                            pass
                    contents.append({
                        "role": "user",
                        "parts": [{
                            "functionResponse": {
                                "name": tool_name,
                                "response": {
                                    "content": content,
                                },
                            },
                        }],
                    })
                else:
                    contents.append({
                        "role": "user",
                        "parts": [{"text": content}],
                    })
                continue

            if role == "assistant":
                tool_calls = msg.get("tool_calls")
                if tool_calls:
                    parts: list[dict[str, Any]] = []
                    if content:
                        parts.append({"text": content})
                    for tc in tool_calls:
                        func = tc.get("function", {})
                        arguments = func.get("arguments", "{}")
                        if isinstance(arguments, str):
                            try:
                                arguments = json.loads(arguments)
                            except json.JSONDecodeError:
                                arguments = {"raw": arguments}
                        parts.append({
                            "functionCall": {
                                "name": func.get("name", ""),
                                "args": arguments,
                            },
                        })
                    contents.append({"role": "model", "parts": parts})
                else:
                    contents.append({
                        "role": "model",
                        "parts": [{"text": content}],
                    })
                continue

            if role == "tool":
                # OpenAI format: {"role": "tool", "tool_call_id": "...", "content": "..."}
                tool_name = msg.get("name", "")
                tool_content = msg.get("content", "")
                contents.append({
                    "role": "user",
                    "parts": [{
                        "functionResponse": {
                            "name": tool_name,
                            "response": {
                                "content": tool_content,
                            },
                        },
                    }],
                })

        return system_instruction, contents

    def _convert_tools(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """将 OpenAI function 格式转换为 Gemini functionDeclarations"""
        declarations = []
        for tool in tools:
            if tool.get("type") == "function":
                func = tool.get("function", {})
                declarations.append({
                    "name": func.get("name", ""),
                    "description": func.get("description", ""),
                    "parameters": func.get("parameters", {"type": "object", "properties": {}}),
                })
        if not declarations:
            return []
        return [{"functionDeclarations": declarations}]

    def _convert_tool_choice(self, tool_choice: str) -> dict[str, Any] | None:
        """将 OpenAI tool_choice 格式转换为 Gemini toolConfig"""
        if tool_choice == "auto":
            return {"functionCallingConfig": {"mode": "AUTO"}}
        elif tool_choice == "required":
            return {"functionCallingConfig": {"mode": "ANY"}}
        elif tool_choice and tool_choice not in ("none",):
            return {"functionCallingConfig": {"mode": "AUTO"}}
        if tool_choice == "none":
            return {"functionCallingConfig": {"mode": "NONE"}}
        return None

    def _parse_response(self, data: dict[str, Any], model: str) -> LLMResponse:
        """将 Gemini 响应转换为 LLMResponse"""
        candidates = data.get("candidates", [])
        text_parts: list[str] = []
        tool_calls: list[ToolCall] = []
        tool_call_index = 0

        if candidates:
            content = candidates[0].get("content", {})
            parts = content.get("parts", [])
            for part in parts:
                if "text" in part:
                    text_parts.append(part["text"])
                elif "functionCall" in part:
                    fc = part["functionCall"]
                    tool_calls.append(ToolCall(
                        id=f"call_{tool_call_index}",
                        name=fc.get("name", ""),
                        arguments=fc.get("args", {}),
                    ))
                    tool_call_index += 1

        usage_metadata = data.get("usageMetadata", {})
        usage = TokenUsage(
            prompt_tokens=usage_metadata.get("promptTokenCount", 0),
            completion_tokens=usage_metadata.get("candidatesTokenCount", 0),
        )

        return LLMResponse(
            content="".join(text_parts),
            model=data.get("modelVersion", model),
            usage=usage,
            tool_calls=tool_calls,
        )

    def _handle_error(self, status_code: int, resp_body: bytes) -> None:
        """处理 Gemini API 错误响应"""
        try:
            error_data = json.loads(resp_body)
            error_info = error_data.get("error", {})
            error_msg = error_info.get("message", f"HTTP {status_code}")
        except (json.JSONDecodeError, AttributeError):
            error_msg = f"HTTP {status_code}"

        raise LLMProviderError("gemini", f"HTTP {status_code}: {error_msg}")

    async def chat(self, request: LLMRequest) -> LLMResponse:
        """发送 chat 请求（带 retry + circuit breaker）"""
        if self._circuit_breaker and self._retry_policy:
            return await self._circuit_breaker.execute(
                self._retry_policy.execute, self._chat_impl, request
            )
        if self._retry_policy:
            return await self._retry_policy.execute(self._chat_impl, request)
        if self._circuit_breaker:
            return await self._circuit_breaker.execute(self._chat_impl, request)
        return await self._chat_impl(request)

    async def _chat_impl(self, request: LLMRequest) -> LLMResponse:
        client = self._get_client()
        model = request.model or self._model
        url = f"{self._base_url}/v1beta/models/{model}:generateContent?key={self._api_key}"

        system_instruction, contents = self._convert_messages(request.messages)

        payload: dict[str, Any] = {
            "contents": contents,
            "generationConfig": {
                "temperature": request.temperature,
                "maxOutputTokens": request.max_tokens or self._max_output_tokens,
            },
        }

        if system_instruction is not None:
            payload["systemInstruction"] = system_instruction

        if request.tools:
            gemini_tools = self._convert_tools(request.tools)
            if gemini_tools:
                payload["tools"] = gemini_tools
            tool_config = self._convert_tool_choice(request.tool_choice)
            if tool_config is not None:
                payload["toolConfig"] = tool_config

        if self._safety_settings:
            payload["safetySettings"] = self._safety_settings

        start = time.monotonic()

        try:
            resp = await client.post(url, json=payload)
        except httpx.HTTPError as e:
            raise LLMProviderError("gemini", str(e)) from e

        latency_ms = (time.monotonic() - start) * 1000

        if resp.status_code != 200:
            self._handle_error(resp.status_code, resp.content)

        data = resp.json()
        response = self._parse_response(data, model)
        response.latency_ms = latency_ms

        return response

    async def chat_stream(self, request: LLMRequest):
        """Stream chat response using SSE（带 retry + circuit breaker）"""
        if self._circuit_breaker and self._retry_policy:
            ctx = await self._circuit_breaker.execute(
                self._retry_policy.execute, self._open_stream, request
            )
        elif self._retry_policy:
            ctx = await self._retry_policy.execute(self._open_stream, request)
        elif self._circuit_breaker:
            ctx = await self._circuit_breaker.execute(self._open_stream, request)
        else:
            ctx = await self._open_stream(request)

        async with ctx as response:
            async for chunk in self._iterate_stream(response, request):
                yield chunk

    async def _open_stream(self, request: LLMRequest):
        """Open the streaming HTTP connection; returns an async context manager."""
        client = self._get_client()
        model = request.model or self._model
        url = f"{self._base_url}/v1beta/models/{model}:streamGenerateContent?key={self._api_key}&alt=sse"

        system_instruction, contents = self._convert_messages(request.messages)

        payload: dict[str, Any] = {
            "contents": contents,
            "generationConfig": {
                "temperature": request.temperature,
                "maxOutputTokens": request.max_tokens or self._max_output_tokens,
            },
        }

        if system_instruction is not None:
            payload["systemInstruction"] = system_instruction

        if request.tools:
            gemini_tools = self._convert_tools(request.tools)
            if gemini_tools:
                payload["tools"] = gemini_tools
            tool_config = self._convert_tool_choice(request.tool_choice)
            if tool_config is not None:
                payload["toolConfig"] = tool_config

        if self._safety_settings:
            payload["safetySettings"] = self._safety_settings

        response_ctx = client.stream("POST", url, json=payload)
        response = await response_ctx.__aenter__()

        if response.status_code != 200:
            error_body = await response.aread()
            await response_ctx.__aexit__(None, None, None)
            self._handle_error(response.status_code, error_body)

        return _GeminiStreamContext(response_ctx, response)

    async def _iterate_stream(self, response, request: LLMRequest):
        """Iterate over an already-open SSE stream and yield StreamChunks."""
        accumulated_tool_calls: list[dict[str, Any]] = []
        model = request.model or self._model

        async for line in response.aiter_lines():
            line = line.strip()
            if not line or not line.startswith("data: "):
                continue

            data_str = line[6:]
            try:
                data = json.loads(data_str)
            except json.JSONDecodeError:
                continue

            candidates = data.get("candidates", [])
            if not candidates:
                # Usage-only chunk
                usage_metadata = data.get("usageMetadata")
                if usage_metadata:
                    usage = TokenUsage(
                        prompt_tokens=usage_metadata.get("promptTokenCount", 0),
                        completion_tokens=usage_metadata.get("candidatesTokenCount", 0),
                    )
                    if accumulated_tool_calls:
                        tool_calls = [
                            ToolCall(
                                id=tc["id"],
                                name=tc["name"],
                                arguments=tc["arguments"],
                            )
                            for tc in accumulated_tool_calls
                        ]
                        yield StreamChunk(
                            content="",
                            model=data.get("modelVersion", model),
                            tool_calls=tool_calls,
                            usage=usage,
                            is_final=True,
                        )
                        accumulated_tool_calls = []
                    else:
                        yield StreamChunk(
                            content="",
                            model=data.get("modelVersion", model),
                            usage=usage,
                            is_final=True,
                        )
                continue

            content = candidates[0].get("content", {})
            parts = content.get("parts", [])

            for part in parts:
                if "text" in part:
                    text = part["text"]
                    if text:
                        yield StreamChunk(
                            content=text,
                            model=data.get("modelVersion", model),
                        )
                elif "functionCall" in part:
                    fc = part["functionCall"]
                    accumulated_tool_calls.append({
                        "id": f"call_{len(accumulated_tool_calls)}",
                        "name": fc.get("name", ""),
                        "arguments": fc.get("args", {}),
                    })

            # Check for finish reason
            finish_reason = candidates[0].get("finishReason", "")
            if finish_reason in ("STOP", "MAX_TOKENS") and accumulated_tool_calls:
                tool_calls = [
                    ToolCall(
                        id=tc["id"],
                        name=tc["name"],
                        arguments=tc["arguments"],
                    )
                    for tc in accumulated_tool_calls
                ]
                yield StreamChunk(
                    content="",
                    model=data.get("modelVersion", model),
                    tool_calls=tool_calls,
                    is_final=True,
                )
                accumulated_tool_calls = []

    def get_model_info(self) -> dict[str, Any]:
        """返回 Provider 和模型信息"""
        return {
            "provider": "gemini",
            "model": self._model,
            "max_output_tokens": self._max_output_tokens,
        }
