"""Precipitation radar frames for the Storms tab
(docs/profile_storms_v0_1_design.md section 4).

One adapter interface, one implementation (RainViewer), so the provider can
be replaced before any commercial release -- RainViewer's public API is for
personal/educational use only, without an SLA (spike section d). The server
fetches ``weather-maps.json`` (cached ~5 min for every viewer, retried,
degraded to an explicit UNAVAILABLE); the browser loads the tiles directly
from RainViewer's tile cache (CORS ``*``), so the 100 requests/IP/min limit
counts per viewer, not for the whole server.

Observed frames only: RainViewer discontinued nowcast (no forecast frames),
and its radar tiles exist up to zoom 7 -- from zoom 8 every tile reads "Zoom
Level Not Supported" -- so the layer's maxNativeZoom is 7, not the 10 the
design assumed.
"""

from __future__ import annotations

import json
import os
import threading
import time
from datetime import datetime, timezone
from typing import Any, Callable, Protocol

from mountain_twin.offline import urlopen  # AV-045: offline mode for tests
from mountain_twin.usage import record_cache_hit

RADAR_FLAG_ENVIRONMENT = "AVENTURRO_RADAR"
RADAR_DISABLED_VALUES = frozenset({"0", "off", "false", "disabled", "no"})
RAINVIEWER_MAPS_URL = "https://api.rainviewer.com/public/weather-maps.json"
RAINVIEWER_MAX_NATIVE_ZOOM = 7
RADAR_CACHE_SECONDS = 300
RADAR_FETCH_ATTEMPTS = 2
RADAR_CAPTION = "Radar pokazuje opady zaobserwowane w ostatnich 2 godzinach, nie prognozę."


class RadarProvider(Protocol):
    def frames_document(self) -> dict[str, Any]: ...


def radar_enabled(environment: dict[str, str] | None = None) -> bool:
    value = (environment if environment is not None else os.environ).get(RADAR_FLAG_ENVIRONMENT, "")
    return value.strip().lower() not in RADAR_DISABLED_VALUES


def disabled_radar_document() -> dict[str, Any]:
    return {"state": "DISABLED", "reason": "RADAR_DISABLED_BY_CONFIGURATION", "frames": []}


class RainViewerRadarProvider:
    """RainViewer's weather-maps index, reshaped to the adapter's document:
    each past frame's time and tile URL template, exactly as the index
    gives its hashed path (the newest frame is the last of ``radar.past``)."""

    def __init__(
        self,
        fetch_json: Callable[[str], dict[str, Any]] | None = None,
        clock: Callable[[], float] = time.time,
    ):
        self._fetch_json = fetch_json or _fetch_json
        self._clock = clock
        self._lock = threading.Lock()
        self._cached: tuple[float, dict[str, Any]] | None = None

    def frames_document(self) -> dict[str, Any]:
        with self._lock:
            now = self._clock()
            if self._cached and now - self._cached[0] < RADAR_CACHE_SECONDS:
                record_cache_hit("rainviewer")  # AV-052
                return self._cached[1]
            document = self._fresh_document()
            # A failure is cached briefly too: every viewer does not retry it.
            self._cached = (now, document)
            return document

    def _fresh_document(self) -> dict[str, Any]:
        last_error = None
        for _ in range(RADAR_FETCH_ATTEMPTS):
            try:
                index = self._fetch_json(RAINVIEWER_MAPS_URL)
                return _document_from_index(index)
            except Exception as error:  # network, HTTP, JSON or shape
                last_error = error
        return {
            "state": "UNAVAILABLE",
            "reason": "PROVIDER_UNAVAILABLE",
            "detail": type(last_error).__name__ if last_error else None,
            "provider": "RainViewer",
            "frames": [],
        }


def _document_from_index(index: dict[str, Any]) -> dict[str, Any]:
    host = index["host"].rstrip("/")
    past = index["radar"]["past"]
    frames = [
        {
            "time": datetime.fromtimestamp(int(frame["time"]), timezone.utc).isoformat(),
            "tile_url": f"{host}{frame['path']}/256/{{z}}/{{x}}/{{y}}/2/1_1.png",
        }
        for frame in past
        if frame.get("path")
    ]
    if not frames:
        return {
            "state": "UNAVAILABLE",
            "reason": "PROVIDER_NO_FRAMES",
            "provider": "RainViewer",
            "frames": [],
        }
    return {
        "state": "AVAILABLE",
        "provider": "RainViewer",
        "attribution": {"text": "RainViewer API", "url": "https://www.rainviewer.com/api.html"},
        "kind": "OBSERVED",
        "caption": RADAR_CAPTION,
        "max_native_zoom": RAINVIEWER_MAX_NATIVE_ZOOM,
        "opacity": 0.7,
        "generated": datetime.fromtimestamp(int(index["generated"]), timezone.utc).isoformat()
        if index.get("generated")
        else None,
        "forecast_frame_count": len(index["radar"].get("nowcast") or []),
        "frames": frames,
    }


def _fetch_json(url: str) -> dict[str, Any]:
    with urlopen(url, timeout=10) as response:  # noqa: S310 - fixed https URL
        return json.loads(response.read())


__all__ = [
    "RADAR_CAPTION",
    "RADAR_FLAG_ENVIRONMENT",
    "RAINVIEWER_MAX_NATIVE_ZOOM",
    "RadarProvider",
    "RainViewerRadarProvider",
    "disabled_radar_document",
    "radar_enabled",
]
