"""Payload builders for the SSR content API (backend/api/content_routes.py).

These mirror backend/web/content.py's context builders — same underlying
grading.climatology()/all_time_records()/etc. calls — but return plain JSON-safe
dicts (raw floats, no Markup-wrapped HTML spans, no ContextVar-based unit
rendering) instead of pre-rendered strings, so a caller doesn't need to be a
Jinja/HTML consumer. Transitional duplication with content.py's own logic is
expected here: Stage 3 of the repo-split rewrites content.py to call these
endpoints via HTTP instead of computing in-process, at which point content.py's
copies of this logic go away and this becomes the only implementation.
"""
import datetime

import polars as pl

from api.payloads import OBS_COLS
from data import cities
from data import city_events
from data import grading

MONTHS = ["january", "february", "march", "april", "may", "june",
          "july", "august", "september", "october", "november", "december"]
MONTHS_TITLE = [m.capitalize() for m in MONTHS]
MONTH_INDEX = {m: i + 1 for i, m in enumerate(MONTHS)}

SEASONS = [
    ("Winter", "Summer", [12, 1, 2], "Dec–Feb"),
    ("Spring", "Autumn", [3, 4, 5], "Mar–May"),
    ("Summer", "Winter", [6, 7, 8], "Jun–Aug"),
    ("Autumn", "Spring", [9, 10, 11], "Sep–Nov"),
]

METRIC_LABELS = [
    ("tmax", "High"), ("tmin", "Low"), ("feels", "Feels-like"),
    ("humid", "Humidity"), ("wind", "Wind"), ("gust", "Gust"), ("precip", "Precip"),
]
_TEMP_METRICS = {"tmax", "tmin", "feels"}

# Countries that actually use Fahrenheit. Mirrors content.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"})

# 12 city chips for the homepage, geographically spread — mirrors content.py's
# HOME_CITY_SLUGS exactly (a test asserts the two lists stay identical).
HOME_CITY_SLUGS = (
    "new-york-city-new-york-us", "london-england-gb", "tokyo-jp",
    "sydney-new-south-wales-au", "sao-paulo-br", "lagos-ng",
    "mumbai-maharashtra-in", "mexico-city-mx", "cairo-eg",
    "toronto-ontario-ca", "berlin-state-of-berlin-de", "seattle-washington-us",
)


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


def _month_doy(month_idx: int) -> int:
    return datetime.date(2001, month_idx, 15).timetuple().tm_yday


def _titled(text: str, suffix: str = " · Thermograph", limit: int = 60) -> str:
    return text + suffix if len(text) + len(suffix) <= limit else text


def _clamp_desc(text: str, limit: int = 155) -> str:
    text = " ".join(text.split())
    if len(text) <= limit:
        return text
    return text[:limit].rsplit(" ", 1)[0].rstrip(",;—-") + "…"


def _month_year(date_s) -> str:
    """'1995-07-13' -> 'Jul 1995'. Meta descriptions have ~155 characters to
    spend, so a record's date gives up its day."""
    try:
        return datetime.date.fromisoformat(str(date_s)).strftime("%b %Y")
    except (ValueError, TypeError):
        return str(date_s)


def _temp_text(f, unit: str) -> str:
    if f is None:
        return "—"
    v = round((f - 32) * 5 / 9) if unit == "C" else round(f)
    return f"{v}°{unit}"


def _breadcrumb_jsonld(origin: str, breadcrumb: list[dict]) -> dict:
    """BreadcrumbList structured data. Google requires `item` on every ListItem
    except the last, so unlinked intermediate crumbs (e.g. the country, which
    has no page of its own) are omitted here even though the visible breadcrumb
    shows them."""
    crumbs = [c for c in breadcrumb[:-1] if c["href"]] + breadcrumb[-1:]
    return {"@type": "BreadcrumbList",
            "itemListElement": [
                {"@type": "ListItem", "position": i + 1, "name": c["name"],
                 **({"item": f"{origin}{c['href']}"} if c["href"] else {})}
                for i, c in enumerate(crumbs)]}


def hub_payload() -> dict:
    groups = cities.by_country()
    return {
        "n_cities": sum(len(v) for v in groups.values()),
        "n_countries": len(groups),
        "countries": [
            {"country": country,
             "cities": [{"slug": c["slug"], "name": c["name"],
                        "display": cities.display_name(c)} for c in group]}
            for country, group in groups.items()
        ],
    }


def _monthly_normals_raw(history) -> list[dict]:
    """One row per month: raw high/low/precip means + the range-strip bounds."""
    rows = []
    for i, name in enumerate(MONTHS_TITLE, start=1):
        clim = grading.climatology(history, _month_doy(i))
        tmax, tmin, precip = clim.get("tmax"), clim.get("tmin"), clim.get("precip")
        rows.append({
            "name": name, "slug": MONTHS[i - 1],
            "high_f": tmax["mean"] if tmax else None,
            "low_f": tmin["mean"] if tmin else None,
            "range_lo_f": tmin["p10"] if tmin else None,
            "range_hi_f": tmax["p90"] if tmax else None,
            "precip_f": precip["mean"] if precip else None,
        })
    return rows


def _extreme_raw(metric_rec, key: str) -> dict | None:
    if not metric_rec:
        return None
    return {"value_f": metric_rec[key], "date": metric_rec[f"{key}_date"]}


def _period_records_raw(history, months: list[int]) -> dict:
    if len(months) == 1:
        sub = history.filter(pl.col("date").dt.month() == months[0])
    else:
        sub = history.filter(pl.col("date").dt.month().is_in(months))
    rec = grading.all_time_records(sub) if not sub.is_empty() else {}
    tmax, tmin = rec.get("tmax"), rec.get("tmin")
    return {
        "high": {"warm": _extreme_raw(tmax, "max"), "cold": _extreme_raw(tmax, "min")},
        "low": {"warm": _extreme_raw(tmin, "max"), "cold": _extreme_raw(tmin, "min")},
    }


def _monthly_records_raw(history) -> list[dict]:
    return [
        {"name": name, "slug": MONTHS[i - 1], **_period_records_raw(history, [i])}
        for i, name in enumerate(MONTHS_TITLE, start=1)
    ]


def _seasonal_records_raw(history, lat: float) -> list[dict]:
    south = lat < 0
    return [
        {"name": (south_lbl if south else north_lbl), "span": span,
         **_period_records_raw(history, months)}
        for north_lbl, south_lbl, months, span in SEASONS
    ]


def _today_vs_normal_raw(history, recent) -> dict | None:
    if recent is None or recent.is_empty() or "date" not in recent.columns:
        return None
    today = datetime.date.today()
    observed = recent.filter(pl.col("date") <= today).sort("date")
    if observed.is_empty():
        return None
    row = observed.row(observed.height - 1, named=True)
    obs = {k: row[k] for k in OBS_COLS if k in row}
    graded = grading.grade_day(history, row["date"], obs)
    cards = []
    for key, label in METRIC_LABELS:
        g = graded.get(key)
        if not g:
            continue
        cards.append({
            "label": label, "metric": key, "value_f": g.get("value"),
            "percentile": g.get("percentile"), "grade": g.get("grade"), "cls": g.get("class"),
        })
    date = row["date"]
    return {"date": date.isoformat() if hasattr(date, "isoformat") else str(date), "cards": cards}


def city_payload(request_origin: str, base: str, city: dict, history, recent=None) -> dict:
    """The full /api/v2/content/city/{slug} payload. `recent` is the caller's
    already-fetched recent/forecast bundle (or None to skip today_vs_normal)."""
    display = cities.display_name(city)
    title = cities.title_name(city)
    years = history["date"].dt.year()
    year_range = [int(years.min()), int(years.max())]
    months = _monthly_normals_raw(history)
    warmest = max((m for m in months if m["high_f"] is not None), key=lambda m: m["high_f"], default=None)
    coldest = min((m for m in months if m["low_f"] is not None), key=lambda m: m["low_f"], default=None)
    wettest = max((m for m in months if m["precip_f"] is not None), key=lambda m: m["precip_f"], default=None)
    records = grading.all_time_records(history)
    unit = unit_for_country(city.get("country_code"))
    flavor = cities.flavor(city["slug"])
    event = city_events.get(city["slug"])
    today = _today_vs_normal_raw(history, recent) if recent is not None else None

    breadcrumb = [{"name": "Home", "href": f"{base}/"}, {"name": "Climate", "href": f"{base}/climate"}]
    if city.get("country"):
        breadcrumb.append({"name": city["country"], "href": None})
    breadcrumb.append({"name": city["name"], "href": None})

    page_url = f"{request_origin}{base}/climate/{city['slug']}"
    n_years = year_range[1] - year_range[0]
    jsonld = {
        "@context": "https://schema.org",
        "@graph": [
            {"@type": "Dataset",
             "name": f"{display} climate normals and records",
             "description": f"Average temperatures, precipitation and record highs and lows for "
                            f"{display}, from ~{n_years} years of daily climate history.",
             "url": page_url,
             "temporalCoverage": f"{year_range[0]}/{year_range[1]}",
             "spatialCoverage": {"@type": "Place", "name": display,
                                 "geo": {"@type": "GeoCoordinates",
                                         "latitude": city["lat"], "longitude": city["lon"]}},
             "creator": {"@type": "Organization", "name": "Thermograph"},
             "isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
            _breadcrumb_jsonld(request_origin, breadcrumb),
        ],
    }
    return {
        "city": city, "display": display, "title": title,
        "year_range": year_range, "n_years": n_years,
        "months": months,
        "warmest_month_slug": warmest["slug"] if warmest else None,
        "coldest_month_slug": coldest["slug"] if coldest else None,
        "wettest_month_slug": wettest["slug"] if wettest else None,
        "all_time_records": records,
        "today_vs_normal": today,
        "flavor": flavor, "event": event, "default_unit": unit,
        "breadcrumb": breadcrumb,
        "canonical_path": f"/climate/{city['slug']}",
        "page_title": _titled(f"{title} climate: daily normals, records & how unusual it is now"),
        "page_description": _clamp_desc(
            f"{title} averages highs of {_temp_text(warmest['high_f'], unit) if warmest else '—'} in "
            f"{warmest['name'] if warmest else 'summer'} and lows of "
            f"{_temp_text(coldest['low_f'], unit) if coldest else '—'} in "
            f"{coldest['name'] if coldest else 'winter'}. "
            f"Every day graded against {n_years} years of local history."),
        "jsonld": jsonld,
    }


def month_payload(base: str, city: dict, history, month_idx: int) -> dict:
    name = city["name"]
    title = cities.title_name(city)
    month_name = MONTHS_TITLE[month_idx - 1]
    month_slug = MONTHS[month_idx - 1]
    years = history["date"].dt.year()
    year_range = [int(years.min()), int(years.max())]

    clim = grading.climatology(history, _month_doy(month_idx))
    tmax, tmin, precip = clim.get("tmax"), clim.get("tmin"), clim.get("precip")
    mdf = history.filter(pl.col("date").dt.month() == month_idx)
    mrec = grading.all_time_records(mdf) if not mdf.is_empty() else {}

    records = []
    for key, label in METRIC_LABELS:
        r = mrec.get(key)
        if not r:
            continue
        if key == "precip":
            totals = (mdf.filter(pl.col("precip").is_not_null())
                         .group_by(pl.col("date").dt.year().alias("yr"))
                         .agg(pl.col("precip").sum().alias("total"),
                              pl.len().alias("days"))
                         .filter(pl.col("days") >= 26)
                         .sort("total"))
            if totals.is_empty():
                continue
            lo, hi = totals.row(0, named=True), totals.row(totals.height - 1, named=True)
            records.append({"label": label,
                            "high_f": hi["total"], "high_date": str(hi["yr"]), "high_tag": "Wettest",
                            "low_f": lo["total"], "low_date": str(lo["yr"]), "low_tag": "Driest"})
        else:
            records.append({"label": label,
                            "high_f": r["max"], "high_date": r["max_date"], "high_tag": "Highest",
                            "low_f": r["min"], "low_date": r["min_date"], "low_tag": "Lowest"})

    prev_i = 12 if month_idx == 1 else month_idx - 1
    next_i = 1 if month_idx == 12 else month_idx + 1
    n_years = year_range[1] - year_range[0]
    unit = unit_for_country(city.get("country_code"))
    breadcrumb = [{"name": "Home", "href": f"{base}/"}, {"name": "Climate", "href": f"{base}/climate"},
                  {"name": name, "href": f"{base}/climate/{city['slug']}"},
                  {"name": month_name, "href": None}]
    return {
        "month_name": month_name, "month_slug": month_slug,
        "year_range": year_range, "n_years": n_years,
        "avg_high_f": tmax["mean"] if tmax else None,
        "avg_low_f": tmin["mean"] if tmin else None,
        "typical_high_range_f": [tmax["p10"], tmax["p90"]] if tmax else None,
        "typical_low_range_f": [tmin["p10"], tmin["p90"]] if tmin else None,
        "avg_precip_f": precip["mean"] if precip else None,
        "records": records,
        "prev": {"name": MONTHS_TITLE[prev_i - 1], "slug": MONTHS[prev_i - 1]},
        "next": {"name": MONTHS_TITLE[next_i - 1], "slug": MONTHS[next_i - 1]},
        "breadcrumb": breadcrumb,
        "canonical_path": f"/climate/{city['slug']}/{month_slug}",
        "page_title": _titled(f"{title} in {month_name}: normal weather & records"),
        "page_description": _clamp_desc(
            f"{title} averages {_temp_text(tmax['mean'] if tmax else None, unit)} highs and "
            f"{_temp_text(tmin['mean'] if tmin else None, unit)} lows in {month_name}. "
            f"Every day graded against {n_years} years of local history."),
    }


def records_payload(request_origin: str, base: str, city: dict, history) -> dict:
    display = cities.display_name(city)
    title = cities.title_name(city)
    years = history["date"].dt.year()
    year_range = [int(years.min()), int(years.max())]
    n_years = year_range[1] - year_range[0]
    rec = grading.all_time_records(history)
    hi_rec = (rec.get("tmax") or {}).get("max")
    hi_date = (rec.get("tmax") or {}).get("max_date")
    lo_rec = (rec.get("tmin") or {}).get("min")
    lo_date = (rec.get("tmin") or {}).get("min_date")
    unit = unit_for_country(city.get("country_code"))

    rows = []
    for key, label in METRIC_LABELS:
        r = rec.get(key)
        if not r:
            continue
        if key == "precip":
            # "Record low" rain is meaningless (it's just 0) -- the low side is
            # the longest dry streak instead, which is a day count + start date,
            # not a precip value, so low_f/low_date stay None here.
            days, dry_start = grading.longest_dry_streak(history)
            low_f, low_date = None, dry_start or None
            dry_streak_days = days or None
        else:
            low_f, low_date, dry_streak_days = r["min"], r["min_date"], None
        rows.append({
            "label": label,
            "high_f": r["max"], "high_date": r["max_date"],
            "low_f": low_f, "low_date": low_date,
            "dry_streak_days": dry_streak_days,
        })
    hemisphere = "Southern" if city["lat"] < 0 else "Northern"
    breadcrumb = [{"name": "Home", "href": f"{base}/"}, {"name": "Climate", "href": f"{base}/climate"},
                  {"name": city["name"], "href": f"{base}/climate/{city['slug']}"},
                  {"name": "Records", "href": None}]
    page_url = f"{request_origin}{base}/climate/{city['slug']}/records"
    jsonld = {
        "@context": "https://schema.org",
        "@graph": [
            {"@type": "Dataset",
             "name": f"{display} monthly and seasonal weather records",
             "description": f"Record high and low temperatures for {display} by month and by "
                            f"meteorological season, with the dates they occurred, from ~{n_years} "
                            f"years of daily climate history.",
             "url": page_url,
             "temporalCoverage": f"{year_range[0]}/{year_range[1]}",
             "spatialCoverage": {"@type": "Place", "name": display,
                                 "geo": {"@type": "GeoCoordinates",
                                         "latitude": city["lat"], "longitude": city["lon"]}},
             "creator": {"@type": "Organization", "name": "Thermograph"},
             "isBasedOn": "https://open-meteo.com/ (ERA5 reanalysis)"},
            _breadcrumb_jsonld(request_origin, breadcrumb),
        ],
    }
    return {
        "year_range": year_range, "n_years": n_years,
        "rows": rows,
        "all_time_records": rec,
        "monthly": _monthly_records_raw(history),
        "seasonal": _seasonal_records_raw(history, city["lat"]),
        "hemisphere": hemisphere,
        "breadcrumb": breadcrumb,
        "canonical_path": f"/climate/{city['slug']}/records",
        "page_title": _titled(f"{title} weather records: hottest & coldest days since {year_range[0]}"),
        "page_description": _clamp_desc(
            f"{title}'s hottest day hit {_temp_text(hi_rec, unit)}"
            f"{f' ({_month_year(hi_date)})' if hi_date else ''}; its coldest fell to "
            f"{_temp_text(lo_rec, unit)}{f' ({_month_year(lo_date)})' if lo_date else ''}. "
            f"Every day graded against {n_years} years of local history."),
        "jsonld": jsonld,
    }


def home_payload() -> dict:
    from api import homepage
    feed = homepage.load()
    stale = bool(feed) and homepage.is_stale(feed)
    ranked, unusual = [], None
    if feed:
        ranked = feed.get("ranked") or []
        pick = (feed.get("picks") or {}).get("extreme")
        if pick:
            unusual = dict(pick, is_default=True)
    home_cities = []
    for slug in HOME_CITY_SLUGS:
        city = cities.get(slug)
        if city:
            home_cities.append({"slug": slug, "name": city["name"]})
    return {"unusual": unusual, "stale": stale, "ranked": ranked, "cities": home_cities}
