"""OpenRouter Adapter - Primary LLM provider.

OpenRouter provides a unified API to 200+ models from:
- OpenAI (GPT-4, GPT-4o, etc.)
- Anthropic (Claude 3, Claude 3.5, etc.)
- Google (Gemini Pro, etc.)
- Meta (Llama 3, etc.)
- And many more

Benefits:
- Single API key for all models
- Automatic fallbacks
- Vision, audio, embeddings all supported
- Cost tracking built-in

Model naming: provider/model (e.g., "openai/gpt-4o", "anthropic/claude-3.5-sonnet")
"""

from __future__ import annotations

import json
import logging

import httpx

from core.types.llm import LLMEmbedResult, LLMGenerateResult, LLMToolCall, LLMToolCallFunction

logger = logging.getLogger(__name__)


class OpenRouterAdapter:
    """OpenRouter provider adapter."""

    BASE_URL = "https://openrouter.ai/api/v1"
    DEFAULT_MODEL = "openai/gpt-4o-mini"

    def __init__(self, api_key: str):
        self.api_key = api_key
        self._client: httpx.AsyncClient | None = None

    def _ensure_client(self) -> httpx.AsyncClient:
        if self._client is None:
            from core.config import get_settings

            settings = get_settings()
            self._client = httpx.AsyncClient(
                base_url=self.BASE_URL,
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "HTTP-Referer": settings.openrouter_referer,
                    "X-Title": settings.openrouter_title,
                    "Content-Type": "application/json",
                },
                timeout=settings.openrouter_timeout_s,
            )
        return self._client

    async def aclose(self) -> None:
        """Close the client connection."""
        if self._client is not None:
            await self._client.aclose()
            self._client = None

    def _normalize_model(self, model: str) -> str:
        """Normalize model name to OpenRouter format (provider/model)."""
        if "/" in model:
            return model  # Already in correct format

        # Map common model names to OpenRouter format
        model_lower = model.lower()

        # OpenAI models
        if model_lower.startswith("gpt-"):
            return f"openai/{model}"
        if model_lower.startswith("o1"):
            return f"openai/{model}"
        if model_lower.startswith("text-embedding"):
            return f"openai/{model}"

        # Anthropic models
        if model_lower.startswith("claude"):
            return f"anthropic/{model}"

        # Google models
        if model_lower.startswith("gemini"):
            return f"google/{model}"

        # Mistral models
        if model_lower.startswith(("mistral", "ministral", "codestral", "devstral")):
            return f"mistralai/{model}"

        # DeepSeek models
        if model_lower.startswith("deepseek"):
            return f"deepseek/{model}"

        # Default: assume OpenAI
        return f"openai/{model}"

    async def generate(
        self,
        *,
        prompt: str = "",
        model: str | None = None,
        system: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        tools: list[dict[str, object]] | None = None,
        tool_choice: str | dict[str, object] | None = None,
        messages: list[dict[str, object]] | None = None,
        images: list[str] | None = None,
        response_format: str = "text",
    ) -> LLMGenerateResult:
        """Generate text using OpenRouter.

        Args:
            prompt: User prompt (used if messages not provided)
            model: Model name (e.g., "openai/gpt-4o", "anthropic/claude-3.5-sonnet")
            system: System prompt
            temperature: Sampling temperature
            max_tokens: Maximum output tokens
            tools: Tool definitions for function calling
            tool_choice: Tool choice constraint
            messages: Chat messages (overrides prompt)
            images: Base64 images for vision
            response_format: "text" or "json"

        Returns
        -------
            LLMGenerateResult with response
        """
        # Import here to avoid circular import at module load time
        from core.llm.providers import LLMProviderError

        client = self._ensure_client()
        model = self._normalize_model(model or self.DEFAULT_MODEL)

        # Build messages
        if messages:
            chat_messages = list(messages)
        else:
            chat_messages = []
            if system:
                chat_messages.append({"role": "system", "content": system})

            # Handle vision (images)
            if images:
                content: list[dict[str, object]] = [{"type": "text", "text": prompt}]
                for img in images:
                    if img.startswith("data:"):
                        content.append({"type": "image_url", "image_url": {"url": img}})
                    else:
                        # Assume base64, add data URL prefix
                        content.append(
                            {
                                "type": "image_url",
                                "image_url": {"url": f"data:image/jpeg;base64,{img}"},
                            },
                        )
                chat_messages.append({"role": "user", "content": content})
            else:
                chat_messages.append({"role": "user", "content": prompt})

        # Build request
        request_data: dict[str, object] = {
            "model": model,
            "messages": chat_messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
        }

        if tools:
            request_data["tools"] = tools
            if tool_choice:
                request_data["tool_choice"] = tool_choice

        if response_format == "json":
            request_data["response_format"] = {"type": "json_object"}

        # Debug: log tool schemas being sent
        if tools:
            tool_names = [t.get("function", {}).get("name", "?") for t in tools]
            logger.info(
                "[OpenRouter] Sending %d tools: %s  tool_choice=%r  model=%s",
                len(tools),
                tool_names[:10],
                tool_choice,
                model,
            )

        try:
            response = await client.post("/chat/completions", json=request_data)
            response.raise_for_status()
            data = response.json()
        except httpx.HTTPStatusError as e:
            error_body = e.response.text
            try:
                error_data = json.loads(error_body)
                error_msg = error_data.get("error", {}).get("message", error_body)
            except json.JSONDecodeError:
                error_msg = error_body
            raise LLMProviderError(
                message=f"OpenRouter API error: {error_msg}",
                provider="openrouter",
                model=model,
                is_transient=e.response.status_code >= 500,
            ) from e
        except Exception as e:
            raise LLMProviderError(
                message=str(e),
                provider="openrouter",
                model=model,
                is_transient=True,
            ) from e

        # Parse response
        choice = data.get("choices", [{}])[0]
        message = choice.get("message", {})
        content = message.get("content", "")
        finish_reason = choice.get("finish_reason")

        # Debug: log tool calls from response
        if tools:
            logger.info(
                "[OpenRouter] Response: finish_reason=%r  has_tool_calls=%s  content_len=%d",
                finish_reason,
                bool(message.get("tool_calls")),
                len(content or ""),
            )

        # Extract tool calls if present — as LLMToolCall Pydantic models
        tool_calls: list[LLMToolCall] | None = None
        if message.get("tool_calls"):
            tool_calls = []
            for tc in message["tool_calls"]:
                func_data = tc.get("function", {})
                tool_calls.append(
                    LLMToolCall(
                        id=tc.get("id", ""),
                        function=LLMToolCallFunction(
                            name=func_data.get("name", ""),
                            arguments=func_data.get("arguments", "{}"),
                        ),
                    ),
                )

        # Extract usage
        usage = data.get("usage", {})
        input_tokens = usage.get("prompt_tokens", 0)
        output_tokens = usage.get("completion_tokens", 0)
        total_tokens = usage.get("total_tokens", input_tokens + output_tokens)

        # Cost (OpenRouter includes this)
        # Note: OpenRouter may return cost in the response headers or usage
        cost_usd = 0.0  # OpenRouter handles billing separately

        return LLMGenerateResult(
            content=content or "",
            model=data.get("model", model),
            tokens_used=total_tokens,
            cost_usd=cost_usd,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
            provider="openrouter",
            finish_reason=finish_reason,
            tool_calls=tool_calls,
            metadata={"id": data.get("id")},
        )

    async def generate_stream(
        self,
        *,
        prompt: str = "",
        model: str | None = None,
        system: str | None = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        tools: list[dict[str, object]] | None = None,
        tool_choice: str | dict[str, object] | None = None,
        messages: list[dict[str, object]] | None = None,
        images: list[str] | None = None,
        response_format: str = "text",
    ):
        """Stream text generation via OpenRouter SSE.

        Yields
        ------
        LLMStreamChunk
            Incremental chunks with ``delta`` text and optional ``tool_calls``.
        """
        from core.llm.providers import LLMProviderError
        from core.types.llm import LLMStreamChunk

        client = self._ensure_client()
        model = self._normalize_model(model or self.DEFAULT_MODEL)

        # Build messages (same logic as generate)
        if messages:
            chat_messages = list(messages)
        else:
            chat_messages = []
            if system:
                chat_messages.append({"role": "system", "content": system})
            if images:
                content: list[dict[str, object]] = [{"type": "text", "text": prompt}]
                for img in images:
                    if img.startswith("data:"):
                        content.append({"type": "image_url", "image_url": {"url": img}})
                    else:
                        content.append(
                            {
                                "type": "image_url",
                                "image_url": {"url": f"data:image/jpeg;base64,{img}"},
                            },
                        )
                chat_messages.append({"role": "user", "content": content})
            else:
                chat_messages.append({"role": "user", "content": prompt})

        request_data: dict[str, object] = {
            "model": model,
            "messages": chat_messages,
            "temperature": temperature,
            "max_tokens": max_tokens,
            "stream": True,
        }
        if tools:
            request_data["tools"] = tools
            if tool_choice:
                request_data["tool_choice"] = tool_choice
        if response_format == "json":
            request_data["response_format"] = {"type": "json_object"}

        try:
            async with client.stream("POST", "/chat/completions", json=request_data) as resp:
                resp.raise_for_status()
                async for line in resp.aiter_lines():
                    if not line.startswith("data: "):
                        continue
                    payload = line[len("data: ") :]
                    if payload.strip() == "[DONE]":
                        break
                    chunk_data = json.loads(payload)
                    choice = (chunk_data.get("choices") or [{}])[0]
                    delta = choice.get("delta", {})
                    finish = choice.get("finish_reason")

                    # Parse incremental tool calls
                    tc_list = None
                    if delta.get("tool_calls"):
                        tc_list = [
                            LLMToolCall(
                                id=tc.get("id", ""),
                                function=LLMToolCallFunction(
                                    name=tc.get("function", {}).get("name", ""),
                                    arguments=tc.get("function", {}).get("arguments", ""),
                                ),
                            )
                            for tc in delta["tool_calls"]
                        ]

                    yield LLMStreamChunk(
                        delta=delta.get("content", "") or "",
                        finish_reason=finish,
                        tool_calls=tc_list,
                    )
        except httpx.HTTPStatusError as e:
            error_body = e.response.text
            try:
                error_data = json.loads(error_body)
                error_msg = error_data.get("error", {}).get("message", error_body)
            except json.JSONDecodeError:
                error_msg = error_body
            raise LLMProviderError(
                message=f"OpenRouter streaming API error: {error_msg}",
                provider="openrouter",
                model=model,
                is_transient=e.response.status_code >= 500,
            ) from e
        except Exception as e:
            if isinstance(e, LLMProviderError):
                raise
            raise LLMProviderError(
                message=str(e),
                provider="openrouter",
                model=model,
                is_transient=True,
            ) from e

    async def embed(
        self,
        texts: list[str],
        *,
        model: str = "openai/text-embedding-3-small",
    ) -> LLMEmbedResult:
        """Generate embeddings using OpenRouter.

        Args:
            texts: Texts to embed
            model: Embedding model

        Returns
        -------
            LLMEmbedResult with vectors
        """
        from core.llm.providers import LLMProviderError

        client = self._ensure_client()
        model = self._normalize_model(model)

        request_data = {
            "model": model,
            "input": texts,
        }

        try:
            response = await client.post("/embeddings", json=request_data)
            response.raise_for_status()
            data = response.json()
        except httpx.HTTPStatusError as e:
            raise LLMProviderError(
                message=f"OpenRouter embedding error: {e.response.text}",
                provider="openrouter",
                model=model,
                is_transient=e.response.status_code >= 500,
            ) from e
        except Exception as e:
            raise LLMProviderError(
                message=str(e),
                provider="openrouter",
                model=model,
                is_transient=True,
            ) from e

        vectors = [item["embedding"] for item in data.get("data", [])]
        usage = data.get("usage", {})
        tokens_used = usage.get("total_tokens", 0)

        return LLMEmbedResult(
            vectors=vectors,
            model=data.get("model", model),
            tokens_used=tokens_used,
            cost_usd=0.0,
            provider="openrouter",
        )


__all__ = ["OpenRouterAdapter"]
