"""OpenAI-compatible chat completions provider."""

from __future__ import annotations

import json
import socket
import urllib.error
import urllib.request
import uuid
from dataclasses import dataclass
from typing import Any

from memnixa.context_window import estimate_prompt_tokens
from memnixa.models import LLMResponse, ToolCall
from memnixa.providers.base import LLMProvider, ProviderSpec
from memnixa.providers.compat import build_request_payload


@dataclass(slots=True)
class OpenAICompatibleProvider(LLMProvider):
    """Provider for OpenAI-style `/chat/completions` endpoints."""

    spec: ProviderSpec
    api_key: str
    api_base: str
    _model_name: str
    timeout: int = 120

    @property
    def model_name(self) -> str:
        return self._model_name

    def estimate_prompt_tokens(
        self, messages: list[dict], tools: list[dict] | None = None
    ) -> tuple[int, str] | None:
        return estimate_prompt_tokens(messages, tools), "memnixa_estimator"

    def chat(
        self, messages: list[dict], tools: list[dict] | None = None
    ) -> LLMResponse:
        payload = build_request_payload(self.model_name, messages, tools)

        request = urllib.request.Request(
            url=f"{self.api_base.rstrip('/')}/chat/completions",
            data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
                "User-Agent": "Memnixa/0.1.0",
                "X-Session-Affinity": uuid.uuid4().hex,
            },
            method="POST",
        )

        try:
            with urllib.request.urlopen(request, timeout=self.timeout) as response:
                raw = json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as exc:
            body = exc.read().decode("utf-8", errors="replace")
            return LLMResponse(
                content=f"HTTP {exc.code}: {body}", finish_reason="error"
            )
        except urllib.error.URLError as exc:
            return LLMResponse(content=f"Network error: {exc}", finish_reason="error")
        except (TimeoutError, socket.timeout):
            return LLMResponse(
                content=f"Network timeout after {self.timeout}s",
                finish_reason="error",
            )

        return self._parse_response(raw)

    @staticmethod
    def _parse_response(raw: dict[str, Any]) -> LLMResponse:
        choices = raw.get("choices") or []
        if not choices:
            return LLMResponse(
                content=f"Malformed response: {raw}", finish_reason="error", raw=raw
            )

        choice = choices[0]
        message = choice.get("message") or {}
        tool_calls: list[ToolCall] = []
        for tool_call in message.get("tool_calls") or []:
            function = tool_call.get("function") or {}
            arguments = function.get("arguments") or "{}"
            parsed_arguments = OpenAICompatibleProvider._load_arguments(arguments)
            tool_calls.append(
                ToolCall(
                    id=str(tool_call.get("id") or uuid.uuid4().hex),
                    name=str(function.get("name") or ""),
                    arguments=parsed_arguments,
                )
            )

        content = message.get("content")
        if isinstance(content, list):
            content = "\n".join(
                part.get("text", "")
                for part in content
                if isinstance(part, dict) and part.get("type") == "text"
            ).strip()

        return LLMResponse(
            content=content if isinstance(content, str) else None,
            tool_calls=tool_calls,
            finish_reason=str(choice.get("finish_reason") or "stop"),
            usage=raw.get("usage") if isinstance(raw.get("usage"), dict) else None,
            raw=raw,
        )

    @staticmethod
    def _load_arguments(arguments: Any) -> dict[str, Any]:
        if isinstance(arguments, dict):
            return arguments
        if not isinstance(arguments, str) or not arguments.strip():
            return {}
        try:
            parsed = json.loads(arguments)
        except json.JSONDecodeError:
            return {"raw": arguments}
        return parsed if isinstance(parsed, dict) else {"value": parsed}
