"""OG image generation — 400×400 track-on-dark PNG for social link previews."""

from __future__ import annotations

import io
import math
from pathlib import Path
from typing import Optional

# Colour stops matching ActivityMap.svelte _linearColor stops
_STOPS: list[tuple[float, tuple[int, int, int]]] = [
    (0.00, (59,  130, 246)),   # blue-500   (low)
    (0.33, (74,  222, 128)),   # green-400
    (0.66, (250, 204,  21)),   # yellow-400
    (1.00, (239,  68,  68)),   # red-500    (high)
]

_BG    = (9, 9, 11)   # zinc-950
_SIZE  = 400
_PAD   = 28
_WIDTH = 5   # logical line width; rendered at 2× then downscaled


def _lerp_color(t: float) -> tuple[int, int, int]:
    t = max(0.0, min(1.0, t))
    for i in range(len(_STOPS) - 1):
        t0, c0 = _STOPS[i]
        t1, c1 = _STOPS[i + 1]
        if t <= t1:
            f = (t - t0) / (t1 - t0) if t1 > t0 else 0.0
            return (
                round(c0[0] + f * (c1[0] - c0[0])),
                round(c0[1] + f * (c1[1] - c0[1])),
                round(c0[2] + f * (c1[2] - c0[2])),
            )
    return _STOPS[-1][1]


def generate(
    lat_arr: list[Optional[float]],
    lon_arr: list[Optional[float]],
    ele_arr: list[Optional[float]],
) -> bytes:
    """Return PNG bytes for a 400×400 elevation-coloured track image.

    Any of the three arrays may have None gaps (no-GPS seconds).
    Returns a plain dark square if there are fewer than 2 valid GPS points.
    """
    try:
        from PIL import Image, ImageDraw  # type: ignore[import]
    except ImportError as e:
        raise RuntimeError("Pillow is required for OG image generation") from e

    # Collect valid GPS points paired with elevation (None → 0 for colouring)
    pts: list[tuple[float, float, float]] = []
    for lat, lon, ele in zip(lat_arr, lon_arr, ele_arr):
        if lat is not None and lon is not None:
            pts.append((float(lat), float(lon), float(ele) if ele is not None else 0.0))

    if len(pts) < 2:
        img = Image.new("RGB", (_SIZE, _SIZE), _BG)
        buf = io.BytesIO()
        img.save(buf, "PNG", optimize=True)
        return buf.getvalue()

    lats = [p[0] for p in pts]
    lons = [p[1] for p in pts]
    eles = [p[2] for p in pts]

    min_lat, max_lat = min(lats), max(lats)
    min_lon, max_lon = min(lons), max(lons)
    min_ele, max_ele = min(eles), max(eles)
    ele_range = max_ele - min_ele or 1.0

    # Mercator correction: compress longitude range by cos(mid_lat) so the
    # track doesn't look stretched horizontally at higher latitudes.
    cos_lat = math.cos(math.radians((min_lat + max_lat) / 2))

    usable   = _SIZE - 2 * _PAD
    lat_span = max_lat - min_lat or 1e-6
    lon_span = (max_lon - min_lon) * cos_lat or 1e-6
    scale    = min(usable / lat_span, usable / lon_span)

    # Centre the track within the canvas
    x_off = _PAD + (usable - (max_lon - min_lon) * cos_lat * scale) / 2
    y_off = _PAD + (usable - lat_span * scale) / 2

    def project(lat: float, lon: float) -> tuple[float, float]:
        x = x_off + (lon - min_lon) * cos_lat * scale
        y = _SIZE - (y_off + (lat - min_lat) * scale)
        return x, y

    # Render at 2× resolution then downscale for cheap anti-aliasing
    S  = _SIZE * 2
    lw = _WIDTH * 2
    img  = Image.new("RGB", (S, S), _BG)
    draw = ImageDraw.Draw(img)

    for i in range(len(pts) - 1):
        x0, y0 = project(pts[i][0],   pts[i][1])
        x1, y1 = project(pts[i+1][0], pts[i+1][1])
        t      = (eles[i] - min_ele) / ele_range
        color  = _lerp_color(t)
        draw.line([(x0 * 2, y0 * 2), (x1 * 2, y1 * 2)], fill=color, width=lw)

    img = img.resize((_SIZE, _SIZE), Image.LANCZOS)

    buf = io.BytesIO()
    img.save(buf, "PNG", optimize=True)
    return buf.getvalue()


def generate_for_activity(ts_path: Path) -> bytes:
    """Convenience wrapper: read a .timeseries.json file and call generate()."""
    import json
    ts = json.loads(ts_path.read_text(encoding="utf-8"))
    return generate(
        ts.get("lat") or [],
        ts.get("lon") or [],
        ts.get("elevation_m") or [],
    )
