"""Ollama tag resolution helpers.

Handles the gap between the user-facing ``deva-coder`` model ID and the
actual Ollama tag installed on a user's machine. Supports two installation
paths:

- **Public registry pull** (``summereunann/deva-coder:latest``) -- what end
  users get when Deva IDE's model picker pulls the published model.
- **Local Modelfile build** (``deva-coder:latest``) -- what developers produce
  when training or quantizing locally via
  ``resources/devseccodingagent/security-llm/Modelfile``.

At chat time the backend queries Ollama's ``/api/tags`` and picks whichever
variant is actually installed, so both paths Just Work with no extra config.
"""
from __future__ import annotations

import os
import time
from typing import Iterable

import httpx


_OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/")

# Candidate tag lists in priority order. The resolver picks the first
# candidate that ``/api/tags`` reports as installed.
_DEVA_CODER_CANDIDATES: dict[str, tuple[str, ...]] = {
	"summereunann/deva-coder:latest": (
		"summereunann/deva-coder:latest",
		"deva-coder:latest",
	),
}

_CACHE_TTL_SECONDS = 30.0
_cache: dict[str, tuple[float, str]] = {}


def strip_namespace(model: str) -> str:
	"""Strip the Ollama registry namespace from a tag.

	``summereunann/deva-coder:latest`` -> ``deva-coder:latest``
	``deva-coder:latest``              -> ``deva-coder:latest``
	"""
	return model.split("/", 1)[1] if "/" in model else model


def matches_prefix(model: str, prefixes: Iterable[str]) -> bool:
	"""True if the model (or its namespace-stripped basename) startswith any prefix.

	Case-insensitive. Used to classify models by family (``deva-coder``,
	``qwen``, etc.) regardless of whether they came from a registry.
	"""
	lower = model.lower()
	basename = strip_namespace(lower)
	return any(basename.startswith(p) or lower.startswith(p) for p in prefixes)


async def resolve_installed_tag(model: str) -> str:
	"""Resolve a model tag to whichever variant is installed in local Ollama.

	For deva-coder tags, queries ``/api/tags`` and returns the first installed
	candidate. Non-deva-coder tags are returned unchanged. If discovery fails
	or nothing is installed, returns the original tag so the downstream error
	message steers the user toward the canonical registry model.
	"""
	candidates = _DEVA_CODER_CANDIDATES.get(model)
	if candidates is None:
		return model

	now = time.monotonic()
	cached = _cache.get(model)
	if cached and (now - cached[0]) < _CACHE_TTL_SECONDS:
		return cached[1]

	try:
		async with httpx.AsyncClient() as client:
			response = await client.get(f"{_OLLAMA_URL}/api/tags", timeout=3.0)
			response.raise_for_status()
			installed = {m.get("name", "").lower() for m in response.json().get("models", [])}
		for tag in candidates:
			if tag.lower() in installed:
				_cache[model] = (now, tag)
				return tag
	except Exception:
		pass

	_cache[model] = (now, model)
	return model


def clear_cache() -> None:
	"""Clear the resolver cache -- useful after a model pull completes."""
	_cache.clear()
