"""Unit-aware formatting for the SSR templates — ported from
backend/web/content.py, unchanged in behavior (still Markup-wrapped spans for
the templates), but operating on raw floats the content API returns instead of
live polars frames. The ContextVar-based active-unit scoping is unchanged: the
templates and context builders both need it (see content.py's `_unit_scope`),
and threading a `unit` parameter through every call site risks a silently
wrong unit at the one site someone forgets, same reasoning as the original.
"""
import contextlib
import contextvars
import math

from markupsafe import Markup

# Countries that actually use Fahrenheit. Mirrors backend/api/content_payloads.py's
# F_COUNTRIES / frontend/units.js's F_REGIONS -- a test asserts all three stay
# identical.
F_COUNTRIES = frozenset({"US", "PR", "GU", "VI", "AS", "MP", "UM",
                         "BS", "BZ", "KY", "PW", "FM", "MH", "LR"})

MM_PER_IN = 25.4
KMH_PER_MPH = 1.609344

# Absolute-temperature colour tiers (°F upper bounds), the same 9 tiers the
# interactive grader uses.
_TEMP_TIERS = [
    (20, "rec-cold"), (32, "very-cold"), (45, "cold"), (58, "cool"),
    (70, "normal"), (80, "warm"), (90, "hot"), (100, "very-hot"),
]
_AXIS_LO, _AXIS_HI = -10.0, 115.0

_UNIT: contextvars.ContextVar[str | None] = contextvars.ContextVar("display_unit", default=None)


@contextlib.contextmanager
def unit_scope(unit: str | None):
    """Render temperatures in `unit` for the duration of the block. The reset
    is not optional -- see content.py's page handlers (sync def -> a recycled
    threadpool thread) for why a leaked value would bleed into the next
    request."""
    token = _UNIT.set(unit)
    try:
        yield
    finally:
        _UNIT.reset(token)


def unit_for_country(code: str | None) -> str:
    return "F" if (code or "").upper() in F_COUNTRIES else "C"


def current_unit() -> str | None:
    return _UNIT.get()


def _round_half_up(x: float) -> int:
    """Round like JS's Math.round, not Python's round() (which rounds halves
    to even) -- so the server and climate.js's repaint never visibly disagree."""
    return math.floor(x + 0.5)


def _c(f: float) -> int:
    return _round_half_up((f - 32) * 5 / 9)


def _shown(f: float) -> int:
    return _c(f) if _UNIT.get() == "C" else _round_half_up(f)


def _letter() -> str:
    return "C" if _UNIT.get() == "C" else "F"


def temp(f) -> Markup | str:
    if f is None:
        return "—"
    return Markup('<span class="temp" data-temp-f="{:.1f}">{}°{}</span>').format(
        f, _shown(f), _letter())


def temp_bare(f) -> Markup | str:
    if f is None:
        return "—"
    return Markup('<span class="temp" data-temp-f="{:.1f}" data-bare>{}°</span>').format(
        f, _shown(f))


def temp_text(f) -> str:
    if f is None:
        return "—"
    return f"{_shown(f)}°{_letter()}"


def precip(v) -> Markup | str:
    if v is None:
        return "—"
    return Markup('<span class="precip" data-precip-in="{:.3f}">{}</span>').format(
        v, precip_text(v))


def precip_text(v) -> str:
    if v is None:
        return "—"
    return (f"{_round_half_up(v * MM_PER_IN)} mm" if _UNIT.get() == "C"
            else f"{v:.2f} in")


def wind(v) -> Markup | str:
    if v is None:
        return "—"
    return Markup('<span class="wind" data-wind-mph="{:.1f}">{}</span>').format(
        v, wind_text(v))


def wind_text(v) -> str:
    if v is None:
        return "—"
    return (f"{_round_half_up(v * KMH_PER_MPH)} km/h" if _UNIT.get() == "C"
            else f"{_round_half_up(v)} mph")


_TEMP_METRICS = {"tmax", "tmin", "feels"}


def fmt(metric: str, v) -> Markup | str:
    if v is None:
        return "—"
    if metric in _TEMP_METRICS:
        return temp(v)
    if metric == "precip":
        return precip(v)
    if metric == "humid":
        return f"{v:.1f} g/m³"
    return wind(v)  # wind, gust


def temp_class(f) -> str:
    """Diverging-palette tier name for an absolute Fahrenheit temperature (or 'none')."""
    if f is None:
        return "none"
    for upper, cls in _TEMP_TIERS:
        if f < upper:
            return cls
    return "rec-hot"


def pct_ordinal(pct) -> str:
    """A percentile as a display ordinal: 66.4 -> '66th', 99.6 -> '99th'.
    Floored into 1..99 -- an empirical percentile is a rank against the sample,
    so rounding can land on 100 (or 0). Ported from backend/data/grading.py's
    pct_ordinal, which stays canonical for the in-process (Day page, calendar,
    chart) surfaces; this is the SSR-side copy, same formula."""
    try:
        n = min(99, max(1, _round_half_up(float(pct))))
    except (TypeError, ValueError):
        return "—"
    suffix = "th" if 10 <= n % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
    return f"{n}{suffix}"


def range_bar(low_f, high_f) -> dict | None:
    """Geometry for one month's low->high bar on the shared axis: left offset +
    width as percentages, and the tier colour at each end. None when missing."""
    if low_f is None or high_f is None:
        return None
    lo = max(_AXIS_LO, min(_AXIS_HI, low_f))
    hi = max(_AXIS_LO, min(_AXIS_HI, high_f))
    span = _AXIS_HI - _AXIS_LO
    return {
        "left": round((lo - _AXIS_LO) / span * 100, 1),
        "width": round(max(2.0, (hi - lo) / span * 100), 1),
        "c1": temp_class(low_f), "c2": temp_class(high_f),
    }
