import json
import logging
import subprocess
import tempfile

import httpx
from django.conf import settings

logger = logging.getLogger(__name__)

GEMINI_GENERATE_CONTENT_URL = (
    "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
)

OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"
OPENROUTER_CHAT_URL = "https://openrouter.ai/api/v1/chat/completions"

RETRYABLE_HTTP_STATUSES = {429, 500, 502, 503, 504}


def is_transient_error(exc: Exception) -> bool:
    """Return whether ``exc`` represents a temporary provider failure.

    HTTP 429/5xx responses and transport-level errors (connection failures,
    timeouts) are worth retrying. Everything else, such as missing API keys or
    unexpected response shapes, is treated as permanent.
    """
    if isinstance(exc, httpx.HTTPStatusError):
        return exc.response.status_code in RETRYABLE_HTTP_STATUSES
    if isinstance(exc, httpx.TransportError):
        return True
    return False


def retry_after_from(exc: Exception) -> int | None:
    """Return the provider's ``Retry-After`` seconds for an error, if any."""
    if isinstance(exc, httpx.HTTPStatusError):
        raw = exc.response.headers.get("Retry-After")
        if raw:
            try:
                return int(raw)
            except ValueError:
                return None
    return None


def friendly_error_message(exc: Exception) -> str:
    """Turn a provider exception into a human-readable error message."""
    if isinstance(exc, httpx.HTTPStatusError):
        return (
            f"Provider returned HTTP {exc.response.status_code} "
            f"({exc.response.reason_phrase})"
        )
    if isinstance(exc, httpx.TransportError):
        return f"Network error contacting the provider: {exc}"
    return str(exc)


def agent_prompt(
    prompt: str,
    provider: str | None = None,
    history: list[dict] | None = None,
    model: str | None = None,
) -> dict:
    """Send a prompt to the configured agent provider and return the response.

    ``history`` is a list of prior turns as ``{"prompt": str, "response": str}``
    dicts, used as conversation context for follow-up questions.

    Returns a dict with ``text``, ``provider`` and ``model`` keys.
    """
    provider = provider or settings.AGENT_PROVIDER
    if provider == "gemini":
        return gemini_agent_prompt(prompt, history=history, model=model)
    if provider == "opencode":
        return opencode_agent_prompt(prompt, history=history)
    if provider == "openrouter":
        return openrouter_agent_prompt(prompt, history=history, model=model)
    raise ValueError(f"Unknown agent provider: {provider}")


def _gemini_contents(prompt: str, history: list[dict] | None) -> list[dict]:
    contents = []
    for turn in history or []:
        if not turn.get("response"):
            continue
        contents.append({"parts": [{"text": turn["prompt"]}]})
        contents.append({"parts": [{"text": turn["response"]}]})
    contents.append({"parts": [{"text": prompt}]})
    return contents


def gemini_agent_prompt(
    prompt: str, history: list[dict] | None = None, model: str | None = None
) -> dict:
    api_key = settings.GOOGLE_AI_API_KEY
    if not api_key:
        raise ValueError("VROBBLER_GOOGLE_AI_API_KEY is not set")
    model = model or settings.LLM_MODEL

    response = httpx.post(
        GEMINI_GENERATE_CONTENT_URL.format(model=model),
        params={"key": api_key},
        json={"contents": _gemini_contents(prompt, history)},
        timeout=120.0,
    )
    response.raise_for_status()
    data = response.json()

    try:
        text = data["candidates"][0]["content"]["parts"][0]["text"]
    except (KeyError, IndexError) as e:
        raise ValueError(f"Unexpected Gemini response: {data}") from e

    return {"text": text.strip(), "provider": "gemini", "model": model}


def _opencode_prompt_with_history(prompt: str, history: list[dict] | None) -> str:
    if not history:
        return prompt
    lines = ["Previous conversation:"]
    for turn in history:
        if not turn.get("response"):
            continue
        lines.append(f"User: {turn['prompt']}")
        lines.append(f"Assistant: {turn['response']}")
    lines.extend(["", "Continue the conversation and answer the latest question:"])
    return "\n".join(lines) + "\n\n" + prompt


def opencode_agent_prompt(prompt: str, history: list[dict] | None = None) -> dict:
    with tempfile.TemporaryDirectory(prefix="vrobbler-agent-") as tmp_dir:
        try:
            result = subprocess.run(
                [
                    settings.OPENCODE_CMD,
                    "run",
                    "--format",
                    "json",
                    "--dir",
                    tmp_dir,
                    _opencode_prompt_with_history(prompt, history),
                ],
                capture_output=True,
                text=True,
                timeout=900,
            )
        except subprocess.TimeoutExpired as e:
            raise ValueError("opencode agent timed out") from e

    if result.returncode != 0:
        raise ValueError(
            f"opencode exited with {result.returncode}: {result.stderr[:500]}"
        )

    text = _parse_opencode_events(result.stdout)
    if not text:
        raise ValueError("opencode agent returned no text output")
    return {"text": text.strip(), "provider": "opencode", "model": "opencode"}


def _parse_opencode_events(raw: str) -> str:
    parts = []
    for line in raw.splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            event = json.loads(line)
        except json.JSONDecodeError:
            continue
        if event.get("type") != "text":
            continue
        part = event.get("part") or {}
        if part.get("text"):
            parts.append(part["text"])
    return "\n".join(parts)


def openrouter_agent_prompt(
    prompt: str, history: list[dict] | None = None, model: str | None = None
) -> dict:
    api_key = settings.OPENROUTER_API_KEY
    if not api_key:
        raise ValueError("VROBBLER_OPENROUTER_API_KEY is not set")
    model = model or settings.LLM_MODEL

    messages = []
    for turn in history or []:
        if not turn.get("response"):
            continue
        messages.append({"role": "user", "content": turn["prompt"]})
        messages.append({"role": "assistant", "content": turn["response"]})
    messages.append({"role": "user", "content": prompt})

    response = httpx.post(
        OPENROUTER_CHAT_URL,
        headers={"Authorization": f"Bearer {api_key}"},
        json={"model": model, "messages": messages},
        timeout=120.0,
    )
    response.raise_for_status()
    data = response.json()

    try:
        text = data["choices"][0]["message"]["content"]
    except (KeyError, IndexError) as e:
        raise ValueError(f"Unexpected OpenRouter response: {data}") from e

    return {"text": text.strip(), "provider": "openrouter", "model": model}


def list_openrouter_free_models() -> list[dict]:
    """Fetch OpenRouter's model catalog and return the free models.

    A model is considered free when both prompt and completion pricing are $0.
    Returns a list of ``{"provider": str, "id": str, "name": str}`` dicts sorted
    by name.
    """
    response = httpx.get(OPENROUTER_MODELS_URL, timeout=30.0)
    response.raise_for_status()
    data = response.json()

    models = []
    for item in data.get("data", []):
        pricing = item.get("pricing") or {}
        if (
            str(pricing.get("prompt", "1")) == "0"
            and str(pricing.get("completion", "1")) == "0"
        ):
            model_id = item.get("id", "")
            if not model_id:
                continue
            models.append(
                {
                    "provider": "openrouter",
                    "id": model_id,
                    "name": item.get("name") or model_id,
                }
            )
    models.sort(key=lambda m: (m["name"].lower(), m["id"]))
    return models


def list_gemini_agent_models() -> list[dict]:
    """Return the configured Gemini model options for agent sessions.

    Gemini calls do not go through OpenRouter, so the models are only offered
    when ``VROBBLER_GOOGLE_AI_API_KEY`` is set. Returns a list of
    ``{"provider": str, "id": str, "name": str}`` dicts.
    """
    if not settings.GOOGLE_AI_API_KEY:
        return []
    models = []
    for model_id in settings.GEMINI_AGENT_MODELS.split(","):
        model_id = model_id.strip()
        if not model_id:
            continue
        models.append(
            {
                "provider": "gemini",
                "id": model_id,
                "name": model_id,
            }
        )
    return models
