"""Fetches the segment-library catalogue from the TerraLab backend and caches
it locally, mirroring AI Edit's ``prompt_presets_client``.

Endpoint: ``GET /api/ai-segmentation/presets`` (public, no auth) returns
``{"version", "categories": [...], "top_picks": [...]}`` where each preset
carries ``demo_url_before`` / ``demo_url_after`` (relative paths served by
``/api/ai-segmentation/template-demos/<id>/<which>``).

Resilience: a fresh result is cached 1h in QSettings; on failure we fall back
to a stale cache, then to the bundled offline catalogue. A short negative cache
avoids re-hitting the network every time the dialog opens before the endpoint
is deployed.
"""
from __future__ import annotations

import json
import time

from qgis.PyQt.QtCore import QSettings

from . import segmentation_presets as _fallback

_CACHE_KEY = "AISegmentation/server_catalog_v1"
_CACHE_TS_KEY = "AISegmentation/server_catalog_v1_ts"
_NEG_TS_KEY = "AISegmentation/server_catalog_v1_neg_ts"
_CACHE_TTL_S = 3600          # success: 1h
_NEG_TTL_S = 600             # failure: don't retry for 10 min


def base_url() -> str:
    try:
        from ...api.terralab_client import TerraLabClient
        return TerraLabClient().base_url
    except Exception:  # noqa: BLE001
        return "https://terra-lab.ai"


def absolute_demo_url(base: str, relative: str | None) -> str:
    """Resolve a (possibly relative) demo image path against the API base.

    An absolute address in the catalogue is fetched as written, so it goes
    through the same gate as every other served address: https with a host, no
    control characters, nothing that could travel anywhere but the network
    stack. Anything else is dropped rather than fetched.
    """
    if not relative or not isinstance(relative, str):
        return ""
    if "://" in relative[:16]:
        from ..server_dials import safe_web_url

        return safe_web_url(relative, "")
    return f"{base.rstrip('/')}{relative}"


def _is_valid_catalog(data) -> bool:
    return isinstance(data, dict) and isinstance(data.get("categories"), list) and len(data["categories"]) > 0


def _read_cache(settings: QSettings, *, ignore_ttl: bool = False) -> dict | None:
    raw = settings.value(_CACHE_KEY)
    ts = settings.value(_CACHE_TS_KEY)
    if not raw:
        return None
    try:
        if not ignore_ttl:
            if ts is None or (time.time() - float(ts)) >= _CACHE_TTL_S:
                return None
        data = json.loads(raw)
        return data if _is_valid_catalog(data) else None
    except Exception:  # noqa: BLE001
        return None


def fetch_catalog(force: bool = False) -> dict | None:
    """Return the server catalogue (``{categories, top_picks}``) or None.

    Order: fresh cache -> network -> stale cache. Returns None only when there
    has never been a successful fetch (caller then uses the offline catalogue).
    """
    settings = QSettings()
    if not force:
        cached = _read_cache(settings)
        if cached is not None:
            return cached
        # Negative cache: skip the network if a recent fetch already failed.
        neg = settings.value(_NEG_TS_KEY)
        try:
            if neg is not None and (time.time() - float(neg)) < _NEG_TTL_S:
                return _read_cache(settings, ignore_ttl=True)
        except Exception:  # noqa: BLE001  # nosec B110
            pass

    resp = None
    try:
        from ...api.terralab_client import TerraLabClient
        resp = TerraLabClient()._request(
            "GET", "/api/ai-segmentation/presets", timeout_ms=10_000)
    except Exception:  # noqa: BLE001
        resp = None

    if _is_valid_catalog(resp):
        try:
            settings.setValue(_CACHE_KEY, json.dumps(resp))
            settings.setValue(_CACHE_TS_KEY, str(time.time()))
            settings.remove(_NEG_TS_KEY)
        except Exception:  # noqa: BLE001  # nosec B110
            pass
        return resp

    # Network failed or returned an error payload: remember the failure and
    # fall back to whatever stale cache we have.
    try:
        settings.setValue(_NEG_TS_KEY, str(time.time()))
    except Exception:  # noqa: BLE001  # nosec B110
        pass
    return _read_cache(settings, ignore_ttl=True)


def _enrich_categories(cats: list[dict]) -> list[dict]:
    """Backfill a sidebar emoji for any category the source omitted (the server
    catalogue currently sends ``emoji: null``). Never overwrites a provided
    emoji, so a future server-supplied glyph still wins."""
    for cat in cats:
        if isinstance(cat, dict) and not cat.get("emoji"):
            cat["emoji"] = _fallback.category_emoji(cat.get("key", ""))
    return cats


def cached_or_offline_catalog() -> tuple[list[dict], list[str]]:
    """UI-thread-safe catalogue: the cached server catalogue (ignoring TTL) if
    one was ever fetched, else the bundled offline one. NEVER touches the
    network - keeping the library snappy is the background prefetch's job
    (``fetch_catalog(force=True)`` run from a QgsTask). Always non-empty, so the
    gallery opens instantly even on a cold cache."""
    data = _read_cache(QSettings(), ignore_ttl=True)
    if data:
        cats = data.get("categories") or []
        tops = data.get("top_picks") or []
        if cats:
            return _enrich_categories(cats), list(tops)
    return _fallback.fallback_categories(), list(_fallback.TOP_PICKS)
