from __future__ import annotations

import json
import os
import urllib.error
import urllib.request
from dataclasses import dataclass
from typing import Any


def _join_url(base: str, path: str) -> str:
    base = base.rstrip("/")
    path = path.lstrip("/")
    return f"{base}/{path}"


def _parse_optional_float_env(name: str, default: float | None) -> float | None:
    """
    Parse an optional float from environment.

    - Unset: return default
    - Empty/"none"/"null"/"omit": return None (omit from request payload)
    - Otherwise: float(value)
    """
    raw = os.environ.get(name)
    if raw is None:
        return default
    s = str(raw).strip().lower()
    if s in {"", "none", "null", "omit"}:
        return None
    return float(s)


@dataclass
class OpenAICompatClient:
    """
    Minimal OpenAI-compatible client using stdlib urllib.
    Works with OpenAI API and many compatible gateways.
    """

    api_key: str
    base_url: str = "https://api.openai.com/v1"
    timeout_seconds: int = 120
    # If None, don't send `temperature` at all.
    # Default to None for maximum compatibility across OpenAI-compatible backends/models.
    temperature: float | None = None

    def chat_completions(self, *, model: str, messages: list[dict[str, Any]]) -> dict[str, Any]:
        url = _join_url(self.base_url, "/chat/completions")
        payload: dict[str, Any] = {
            "model": model,
            "messages": messages,
            "response_format": {"type": "json_object"},
        }
        if self.temperature is not None:
            payload["temperature"] = float(self.temperature)

        def _do(req_payload: dict[str, Any]) -> dict[str, Any]:
            req = urllib.request.Request(
                url=url,
                method="POST",
                data=json.dumps(req_payload).encode("utf-8"),
                headers={
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {self.api_key}",
                },
            )
            with urllib.request.urlopen(req, timeout=self.timeout_seconds) as resp:
                data = resp.read().decode("utf-8", errors="replace")
                return json.loads(data)
        try:
            return _do(payload)
        except urllib.error.HTTPError as e:
            body = ""
            try:
                body = e.read().decode("utf-8", errors="replace")
            except Exception:
                body = ""
            # Some models/providers reject non-default temperature values (or the field entirely).
            # Best-effort: retry once without sending `temperature`.
            if int(getattr(e, "code", 0) or 0) == 400 and "temperature" in body and "temperature" in payload:
                try:
                    payload2 = {k: v for k, v in payload.items() if k != "temperature"}
                    return _do(payload2)
                except Exception:
                    pass
            raise RuntimeError(f"LLM HTTPError {e.code}: {body}") from e
        except Exception as e:
            raise RuntimeError(f"LLM request failed: {e}") from e


@dataclass
class OllamaClient:
    """
    Minimal Ollama client using /api/chat.
    Docs: https://github.com/ollama/ollama/blob/main/docs/api.md
    """

    base_url: str = "http://127.0.0.1:11434"
    timeout_seconds: int = 180
    # If None, omit the temperature option.
    temperature: float | None = 0.2

    def chat(self, *, model: str, messages: list[dict[str, Any]]) -> dict[str, Any]:
        url = _join_url(self.base_url, "/api/chat")
        options: dict[str, Any] = {"temperature": float(self.temperature)} if self.temperature is not None else {}
        payload = {
            "model": model,
            "messages": messages,
            "stream": False,
            # Force JSON output if possible (Ollama supports `format`)
            "format": "json",
            "options": options,
        }
        req = urllib.request.Request(
            url=url,
            method="POST",
            data=json.dumps(payload).encode("utf-8"),
            headers={"Content-Type": "application/json"},
        )
        try:
            with urllib.request.urlopen(req, timeout=self.timeout_seconds) as resp:
                data = resp.read().decode("utf-8", errors="replace")
                return json.loads(data)
        except urllib.error.HTTPError as e:
            body = ""
            try:
                body = e.read().decode("utf-8", errors="replace")
            except Exception:
                body = ""
            raise RuntimeError(f"Ollama HTTPError {e.code}: {body}") from e
        except Exception as e:
            raise RuntimeError(f"Ollama request failed: {e}") from e


def load_llm_from_env() -> tuple[str, Any, str]:
    """
    Returns (provider, client, model).

    provider:
      - openai_compat: uses OPENAI_* env vars, POST {base_url}/chat/completions
      - ollama: uses OLLAMA_* env vars, POST {base_url}/api/chat
    """
    provider = os.environ.get("LLM_PROVIDER", "openai_compat").strip().lower()
    if provider == "ollama":
        base_url = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434").strip()
        model = os.environ.get("OLLAMA_MODEL", "qwen3:4b").strip()
        temperature = _parse_optional_float_env("OLLAMA_TEMPERATURE", 0.2)
        return provider, OllamaClient(base_url=base_url, temperature=temperature), model

    # default: openai compatible
    api_key = os.environ.get("OPENAI_API_KEY", "").strip()
    if not api_key:
        raise RuntimeError("Missing OPENAI_API_KEY (set LLM_PROVIDER=ollama to use Ollama without a key)")
    base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").strip()
    model = os.environ.get("OPENAI_MODEL", "gpt-4.1-mini").strip()
    # NOTE: default to omitting temperature; only send when explicitly configured.
    temperature = _parse_optional_float_env("OPENAI_TEMPERATURE", None)
    return provider, OpenAICompatClient(api_key=api_key, base_url=base_url, temperature=temperature), model


def extract_json_content(resp: dict[str, Any]) -> Any:
    """
    Extract assistant message content from OpenAI chat.completions response.
    Expected to already be a JSON object (response_format=json_object).
    """
    try:
        content = resp["choices"][0]["message"]["content"]
    except Exception:
        return {"error": "bad_response", "raw": resp}
    try:
        return json.loads(content)
    except Exception:
        return {"error": "non_json_content", "raw": content}


def extract_json_content_ollama(resp: dict[str, Any]) -> Any:
    """
    Extract JSON content from Ollama /api/chat response.
    Expected: resp["message"]["content"] is a JSON string when format=json.
    """
    try:
        content = resp["message"]["content"]
    except Exception:
        return {"error": "bad_response", "raw": resp}
    try:
        return json.loads(content)
    except Exception:
        return {"error": "non_json_content", "raw": content}

