# -*- coding: utf-8 -*-
"""FIT parsing and video-window slicing."""
from datetime import datetime, timedelta, timezone
from pathlib import Path

import fitparse


def parse_fit(path: str, tz_hours: float = 8.0) -> dict:
    tz = timezone(timedelta(hours=tz_hours))
    fit = fitparse.FitFile(str(path))
    records = []
    for msg in fit.get_messages("record"):
        d = {f.name: f.value for f in msg}
        ts = d.get("timestamp")
        if ts is None:
            continue
        records.append({
            "t": ts.replace(tzinfo=timezone.utc).astimezone(tz).isoformat(),
            "dist_m": d.get("distance"),
            "spd_kmh": round(d["speed"] * 3.6, 1) if d.get("speed") is not None else None,
            "alt_m": d.get("enhanced_altitude", d.get("altitude")),
            "hr": d.get("heart_rate"),
            "cad": d.get("cadence"),
            "pwr": d.get("power"),
        })
    return {"records": records, "tz_hours": tz_hours}


def slice_window(fit_data: dict, video_t0_iso: str, duration: float) -> list:
    """records annotated with vt (seconds relative to video start), clipped."""
    t0 = datetime.fromisoformat(video_t0_iso)
    out = []
    for r in fit_data["records"]:
        vt = (datetime.fromisoformat(r["t"]) - t0).total_seconds()
        if -5 <= vt <= duration + 5:
            r2 = dict(r)
            r2["vt"] = round(vt, 1)
            out.append(r2)
    return out


def window_stats(win: list) -> dict:
    def agg(key):
        vals = [r[key] for r in win if r.get(key) is not None]
        if not vals:
            return None
        return {"avg": round(sum(vals) / len(vals), 1), "max": round(max(vals), 1)}
    # Sum only the distance covered by seconds that were actually FILMED.
    # Taking last-minus-first instead counts the rider's progress through the
    # gaps between clips too: a 4-clip project spanning 06:38-07:13 in real
    # time but holding 233 s of footage reported 8.25 km, which at its own
    # stated 12.9 km/h average would have taken 38 minutes. The narration
    # quotes these numbers, so an inflated distance becomes a false claim.
    # NB: the gap must be measured on the REAL clock, not on vt. Clips are
    # laid end-to-end on the output timeline, so vt runs continuously across
    # a boundary (…29, 30…) even where half an hour of unfilmed riding sits
    # in between — only r["t"] reveals it.
    dist = 0.0
    prev = None
    for r in win:
        d, ts = r.get("dist_m"), r.get("t")
        if d is None or not ts:
            continue
        cur = (datetime.fromisoformat(ts), d)
        if prev is not None:
            dt = (cur[0] - prev[0]).total_seconds()
            dd = d - prev[1]
            if 0 < dt <= 2 and dd >= 0:      # contiguous second, sane delta
                dist += dd
        prev = cur
    return {
        "records": len(win),
        "dist_km": round(dist / 1000, 2),
        "spd_kmh": agg("spd_kmh"), "hr": agg("hr"),
        "pwr": agg("pwr"), "cad": agg("cad"),
    }


def detect_highlights(win: list, pw_hi: int = 450, hr_red: int = 178,
                      spd_hi: float = 55.0) -> list:
    """Sustained-run detection over per-second data. Returns
    [{kind, start, end, peak}] sorted by start."""
    by_t = {}
    for r in win:
        by_t.setdefault(int(round(r["vt"])), r)
    tmax = max(by_t) if by_t else 0

    def runs(key, threshold, min_len):
        found, run = [], []
        for t in range(0, tmax + 1):
            v = (by_t.get(t) or {}).get(key)
            if v is not None and v >= threshold:
                run.append((t, v))
            else:
                if len(run) >= min_len:
                    found.append({"start": run[0][0], "end": run[-1][0] + 1,
                                  "peak": max(x[1] for x in run),
                                  "avg": round(sum(x[1] for x in run) / len(run))})
                run = []
        if len(run) >= min_len:
            found.append({"start": run[0][0], "end": run[-1][0] + 1,
                          "peak": max(x[1] for x in run),
                          "avg": round(sum(x[1] for x in run) / len(run))})
        return found

    out = []
    for h in runs("pwr", pw_hi, 3):
        out.append({"kind": "power", **h})
    for h in runs("spd_kmh", spd_hi, 3):
        out.append({"kind": "speed", **h})
    for h in runs("hr", hr_red, 5):
        out.append({"kind": "hr_red", **h})
    return sorted(out, key=lambda x: x["start"])


def detect_highlight_bursts(win: list, duration: float,
                            min_gap: float = 10.0,
                            max_bursts: int | None = None) -> list:
    """Pick the moments worth screenshotting, from data volatility alone.

    Per-second 'excitement' = weighted local range of power (0.45), speed
    (0.2), cadence (0.2) and HR (0.15, wider window since HR moves slowly);
    greedy top-score picks with a suppression radius. Validated against a
    fully-analysed real ride (spikes/auto_interval.py): all 7 known story
    beats were hit blind.

    Returns sorted centre-times (seconds, output timeline).
    """
    by_t = {}
    for r in win:
        t = int(round(r["vt"]))
        if 0 <= t:
            by_t.setdefault(t, r)
    T = min(int(duration), max(by_t) if by_t else 0)
    if T <= 0:
        return []

    def series(key):
        out, last = [], 0.0
        for t in range(T + 1):
            v = (by_t.get(t) or {}).get(key)
            if v is not None:
                last = v
            out.append(last)
        return out

    def volatility(sig, half):
        out = []
        for t in range(len(sig)):
            w = sig[max(0, t - half):t + half + 1]
            out.append(max(w) - min(w))
        m = max(out) or 1
        return [v / m for v in out]

    v_pwr = volatility(series("pwr"), 4)
    v_hr = volatility(series("hr"), 8)
    v_cad = volatility(series("cad"), 4)
    v_spd = volatility(series("spd_kmh"), 4)
    score = [0.45 * a + 0.15 * b + 0.2 * c + 0.2 * d
             for a, b, c, d in zip(v_pwr, v_hr, v_cad, v_spd)]

    if max_bursts is None:
        # ~one burst per 18s of footage, bounded sanely for very short
        # or very long rides
        max_bursts = max(4, min(30, round(duration / 18)))
    picked = []
    for t in sorted(range(len(score)), key=lambda i: -score[i]):
        if len(picked) >= max_bursts:
            break
        if all(abs(t - p) > min_gap for p in picked):
            picked.append(t)
    return sorted(picked)


def per_10s_table(win: list, step: int = 10) -> list:
    by_t = {}
    for r in win:
        by_t.setdefault(int(round(r["vt"])), r)
    rows = []
    for t in range(0, (max(by_t) if by_t else 0) + 1, step):
        r = by_t.get(t)
        if r:
            rows.append({"t": t, "spd": r.get("spd_kmh"), "hr": r.get("hr"),
                         "pwr": r.get("pwr"), "cad": r.get("cad")})
    return rows
