"""Dashboard — at-a-glance training state.

Layout:
    Row 1: 3 PMC tiles (CTL / ATL / TSB) with delta vs 7 days ago + form badge.
    Row 2: PMC chart (CTL line, ATL line, TSB on twin axis).
    Row 3: Weekly volume + ACWR (left), Polarized split (right).
    Row 4: Recent activities table.
"""

from __future__ import annotations

import matplotlib.pyplot as plt
import pandas as pd
import streamlit as st

from openrun.web._helpers import (
    cached_activities,
    cached_pmc,
    cached_weekly_tiz,
    empty_state,
    show_sidebar,
)


st.set_page_config(page_title="Dashboard · openrun", page_icon="📊", layout="wide")
show_sidebar()
st.title("📊 Dashboard")


# ---------------------------------------------------------------------------
# data
# ---------------------------------------------------------------------------

pmc = cached_pmc()
runs = cached_activities(type="running")

if pmc.empty:
    empty_state(
        "No training-load data yet.",
        "Run `openrun-sync` from the terminal or import a Garmin export from the Sync page.",
    )
    st.stop()


# ---------------------------------------------------------------------------
# Row 1 — PMC tiles
# ---------------------------------------------------------------------------

today_row = pmc.iloc[-1]
prev_idx = max(0, len(pmc) - 8)
prev_row = pmc.iloc[prev_idx]
ctl, atl, tsb = today_row["CTL"], today_row["ATL"], today_row["TSB"]
d_ctl = ctl - prev_row["CTL"]
d_atl = atl - prev_row["ATL"]
d_tsb = tsb - prev_row["TSB"]


def _tsb_badge(tsb_value: float) -> tuple[str, str]:
    """Return (label, message) — read straight off the README's TSB table."""
    if tsb_value < -30:
        return "🔴 severely fatigued", "injury risk — back off"
    if tsb_value <= -10:
        return "🟠 productive overload", "heart of a build"
    if tsb_value <= 0:
        return "🟡 balanced building", ""
    if tsb_value <= 10:
        return "🟢 sharpening", ""
    if tsb_value <= 25:
        return "🟢 fresh / peaked", "race-day target window"
    return "🔵 detrained", "taper has gone long — add stimulus"


tile1, tile2, tile3 = st.columns(3)
tile1.metric("CTL (fitness, 42d)", f"{ctl:.1f}", f"{d_ctl:+.1f} vs 7d")
tile2.metric("ATL (fatigue, 7d)",  f"{atl:.1f}", f"{d_atl:+.1f} vs 7d")
tile3.metric("TSB (form)",         f"{tsb:+.1f}", f"{d_tsb:+.1f} vs 7d")
label, hint = _tsb_badge(tsb)
st.caption(f"**Form:** {label}" + (f" — {hint}" if hint else ""))


# ---------------------------------------------------------------------------
# Row 2 — PMC chart
# ---------------------------------------------------------------------------

st.subheader("Fitness / fatigue / form")

window_days = st.select_slider(
    "Window",
    options=[30, 60, 90, 180, 365, len(pmc)],
    value=min(180, len(pmc)),
    format_func=lambda d: f"all ({d}d)" if d == len(pmc) else f"{d}d",
    key="pmc_window",
)
recent = pmc.tail(window_days)

fig, ax1 = plt.subplots(figsize=(11, 4.2))
ax1.plot(recent.index, recent["CTL"], color="#2a9d8f", lw=2.2, label="CTL")
ax1.plot(recent.index, recent["ATL"], color="#e76f51", lw=1.2, alpha=0.85, label="ATL")
ax1.set_ylabel("CTL / ATL (training load)")
ax1.legend(loc="upper left", fontsize=8)
ax2 = ax1.twinx()
tsb_series = recent["TSB"]
ax2.fill_between(recent.index, tsb_series, 0,
                 where=tsb_series >= 0, color="#2a9d8f", alpha=0.15, interpolate=True)
ax2.fill_between(recent.index, tsb_series, 0,
                 where=tsb_series < 0, color="#e76f51", alpha=0.15, interpolate=True)
ax2.axhline(0, color="black", lw=0.4)
ax2.axhline(10, color="#2a9d8f", ls=":", lw=0.8)
ax2.axhline(25, color="#2a9d8f", ls=":", lw=0.8)
ax2.set_ylabel("TSB (form)")
fig.tight_layout()
st.pyplot(fig, clear_figure=True)


# ---------------------------------------------------------------------------
# Row 3 — Weekly volume + Polarized split
# ---------------------------------------------------------------------------

left, right = st.columns(2)

with left:
    st.subheader("Weekly volume")
    if not runs.empty and "distance_km" in runs:
        runs2 = runs.dropna(subset=["start_time_local", "distance_km"]).copy()
        runs2["week"] = runs2["start_time_local"].dt.to_period("W-SUN").dt.start_time
        weekly = runs2.groupby("week")["distance_km"].sum().tail(12)
        if len(weekly) >= 4:
            acute = weekly.tail(1).iloc[0]
            chronic = weekly.tail(4).mean()
            acwr = acute / chronic if chronic else float("nan")
            st.metric("This week", f"{acute:.1f} km",
                      f"4-wk avg {chronic:.1f}")
            st.caption(f"ACWR  **{acwr:.2f}** — sweet spot 0.8–1.3, >1.5 is aggressive ramp")
        st.bar_chart(weekly, height=180, width="stretch")
    else:
        empty_state("No running activities yet.")

with right:
    st.subheader("Polarized split (last 12 weeks)")
    tiz = cached_weekly_tiz(weeks=12)
    if not tiz.empty:
        easy = tiz[["z1_s", "z2_s"]].sum().sum()
        mod = tiz["z3_s"].sum()
        hard = tiz[["z4_s", "z5_s"]].sum().sum()
        total = easy + mod + hard
        if total > 0:
            shares = pd.Series(
                {"easy (Z1+Z2)": easy / total,
                 "moderate (Z3)": mod / total,
                 "hard (Z4+Z5)": hard / total},
            )
            cols = st.columns(3)
            cols[0].metric("easy",     f"{shares['easy (Z1+Z2)']*100:.0f}%")
            cols[1].metric("moderate", f"{shares['moderate (Z3)']*100:.0f}%")
            cols[2].metric("hard",     f"{shares['hard (Z4+Z5)']*100:.0f}%")
            st.caption("Seiler polarised target ≈ 80 / <10 / 20")
            fig2, ax = plt.subplots(figsize=(5, 1.6))
            ax.barh([""], [shares["easy (Z1+Z2)"]], color="#2a9d8f")
            ax.barh([""], [shares["moderate (Z3)"]], left=[shares["easy (Z1+Z2)"]], color="#e9c46a")
            ax.barh([""], [shares["hard (Z4+Z5)"]],
                    left=[shares["easy (Z1+Z2)"] + shares["moderate (Z3)"]], color="#e76f51")
            ax.set_xlim(0, 1)
            ax.set_xticks([0, .25, .5, .75, 1])
            ax.set_xticklabels(["0%", "25%", "50%", "75%", "100%"], fontsize=8)
            ax.set_yticks([])
            for s in ("top", "right", "left"):
                ax.spines[s].set_visible(False)
            fig2.tight_layout()
            st.pyplot(fig2, clear_figure=True)
    else:
        empty_state("Run `openrun-time-in-zone` to populate the zone cache.")


# ---------------------------------------------------------------------------
# Row 4 — Cadence trend
# ---------------------------------------------------------------------------

# Training cue (see NatesNotes.md): hold ~170 spm; body falls back to ~161
# when uncued. Per-run average cadence vs those two reference lines.
CADENCE_TARGET_SPM = 170
CADENCE_NATURAL_SPM = 161

st.subheader("Cadence")
cad_runs = (
    runs.dropna(subset=["start_time_local", "avg_cadence_spm"])
    if "avg_cadence_spm" in runs
    else pd.DataFrame()
)
if cad_runs.empty:
    empty_state("No cadence data yet — it arrives with synced activity details.")
else:
    cad = cad_runs.tail(60).copy()  # runs are sorted ascending by start time
    latest = cad["avg_cadence_spm"].iloc[-1]
    st.caption(
        f"Latest run: **{latest:.0f} spm** · target **{CADENCE_TARGET_SPM}** · "
        f"natural fallback **{CADENCE_NATURAL_SPM}** (last {len(cad)} runs)"
    )
    fig3, ax3 = plt.subplots(figsize=(12, 2.8))
    ax3.scatter(cad["start_time_local"], cad["avg_cadence_spm"],
                s=16, color="#264653", alpha=0.65, label="run avg")
    roll = (
        cad.set_index("start_time_local")["avg_cadence_spm"]
        .rolling("28D").mean()
    )
    ax3.plot(roll.index, roll.values, color="#2a9d8f", lw=2, label="28-day mean")
    ax3.axhline(CADENCE_TARGET_SPM, color="#2a9d8f", ls="--", lw=1, label=f"target {CADENCE_TARGET_SPM}")
    ax3.axhline(CADENCE_NATURAL_SPM, color="#e76f51", ls=":", lw=1, label=f"natural {CADENCE_NATURAL_SPM}")
    ax3.set_ylabel("avg cadence (spm)")
    ax3.legend(fontsize=8, loc="lower right", ncol=4)
    for s in ("top", "right"):
        ax3.spines[s].set_visible(False)
    fig3.tight_layout()
    st.pyplot(fig3, clear_figure=True)


# ---------------------------------------------------------------------------
# Row 5 — Recent activities
# ---------------------------------------------------------------------------

st.subheader("Recent activities")
if runs.empty:
    empty_state("No running activities yet.")
else:
    recent_runs = (
        runs.dropna(subset=["start_time_local"])
        .sort_values("start_time_local", ascending=False)
        .head(10)
        .copy()
    )
    recent_runs["pace"] = recent_runs["pace_min_per_km"].apply(
        lambda p: f"{int(p)}:{int((p - int(p)) * 60):02d}/km" if pd.notna(p) else "—"
    )
    recent_runs["distance"] = recent_runs["distance_km"].round(2).astype(str) + " km"
    recent_runs["cadence"] = recent_runs.get("avg_cadence_spm", pd.Series(dtype=float)).round(0)
    display = recent_runs[[
        "start_time_local", "activity_name", "distance", "pace",
        "avg_hr", "cadence", "training_load", "vo2_max",
    ]].rename(columns={
        "start_time_local": "When",
        "activity_name": "Name",
        "distance": "Distance",
        "pace": "Pace",
        "avg_hr": "Avg HR",
        "cadence": "Cad",
        "training_load": "TL",
        "vo2_max": "VO₂",
    })
    st.dataframe(display, width="stretch", hide_index=True)
