"""Race plan — editable plan rows + projected PMC through race day.

Workflow:
    1. Plan rows live in `race_plan` (week_start = Monday of each week).
    2. If empty, page auto-generates a 17-week ramp ending on the last race.
    3. User edits via `st.data_editor`; on save we upsert by week_start.
    4. `plan_to_daily_load` turns the plan into a daily series (using
       `calibrate_tl_per_km` for the conversion factor).
    5. `banister_forecast` splices history with the forecast and we plot the
       resulting CTL / ATL / TSB through the final race date.
"""

from __future__ import annotations

import re
from datetime import date, timedelta

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

from openrun import (
    banister_forecast,
    daily_training_load_series,
    default_config,
    plan_to_daily_load,
)
from openrun.web._helpers import cached_tl_per_km, get_conn, show_sidebar


st.set_page_config(page_title="Race plan · openrun", page_icon="📈", layout="wide")
show_sidebar()
st.title("📈 Race plan")
st.caption(
    "Edit weekly km / long-run km; the projected PMC redraws on save. Race-day TSB "
    "in **+10 to +25** is the target for an A-race."
)

cfg = default_config()
tl_summary = cached_tl_per_km()


# ---------------------------------------------------------------------------
# Setup row: TL/km defaults
# ---------------------------------------------------------------------------

setup = st.columns(4)
default_tl = tl_summary["median"] if tl_summary["n"] > 0 else 11.0
default_race_tl = cfg.banister.race_day_tl_per_km
tl_per_km = setup[0].number_input(
    "Training TL/km",
    min_value=4.0,
    max_value=25.0,
    value=float(round(default_tl, 1)) if not pd.isna(default_tl) else 11.0,
    step=0.5,
    help="Empirical median from your history.",
)
race_tl = setup[1].number_input(
    "Race-day TL/km",
    min_value=4.0,
    max_value=20.0,
    value=float(default_race_tl),
    step=0.5,
    help="Typically 7 — race effort spreads load over more time than training runs.",
)
setup[2].metric("Calibrated median", f"{tl_summary['median']:.1f}" if tl_summary["n"] else "—",
                f"n={tl_summary['n']}")
setup[3].metric("Calibrated IQR",
                f"{tl_summary['q1']:.1f}–{tl_summary['q3']:.1f}" if tl_summary["n"] else "—")


# ---------------------------------------------------------------------------
# Load plan from DB, or seed a default
# ---------------------------------------------------------------------------

conn = get_conn()
plan_df = pd.read_sql(
    "SELECT week_start, weekly_km, long_run_km, notes FROM race_plan ORDER BY week_start",
    conn,
    parse_dates=["week_start"],
)


def _seed_default_plan(races: tuple[tuple[str, str], ...]) -> pd.DataFrame:
    """Generate a 17-week ramp ending at the last race date.

    A simple Lydiard-ish progression — increasing weekly km plus a 3-week
    block + 1-week step-down, peak ~4 weeks before race, then taper.
    """
    if not races:
        end = pd.Timestamp.today().normalize() + pd.Timedelta(weeks=17)
    else:
        end = pd.Timestamp(max(d for _, d in races)).normalize()

    # Anchor on the Monday of the race week, work back 17 weeks.
    end_monday = end - pd.Timedelta(days=end.weekday())
    start_monday = end_monday - pd.Timedelta(weeks=16)

    rows = []
    base, peak = 30.0, 75.0  # km/week — change defaults via the editor
    for i in range(17):
        wk_start = start_monday + pd.Timedelta(weeks=i)
        # Ramp 1–13: weeks 1–3 build, week 4 step-down, etc.
        if i < 13:
            block_pos = i % 4
            block_idx = i // 4
            week_km = base + (peak - base) * (block_idx * 4 + block_pos) / 12
            if block_pos == 3:
                week_km *= 0.7  # step-down
            long_km = round(min(0.40 * week_km, 32.0), 1)
        elif i < 16:
            # Specific block — hold long-run distance
            week_km = peak * 0.85
            long_km = 32.0
        else:
            # Race week
            week_km = 35.0
            long_km = 30.0  # long-run column doubles as "race distance" for race weeks
        rows.append({
            "week_start": wk_start.date(),
            "weekly_km": round(week_km, 1),
            "long_run_km": long_km,
            "notes": "",
        })
    return pd.DataFrame(rows)


if plan_df.empty:
    st.info("No plan yet — seeding a 17-week default ramp ending on the last race.")
    plan_df = _seed_default_plan(cfg.races)
    plan_df["week_start"] = pd.to_datetime(plan_df["week_start"])


# ---------------------------------------------------------------------------
# Editor
# ---------------------------------------------------------------------------

race_dates_iso = {pd.Timestamp(d).date(): label for label, d in cfg.races}


def _label_row(d: pd.Timestamp) -> str:
    week_days = [(d + timedelta(days=i)).date() for i in range(7)]
    races_in_week = [race_dates_iso[wd] for wd in week_days if wd in race_dates_iso]
    return "  ".join(races_in_week)


plan_edit = plan_df.copy()
plan_edit["race"] = plan_edit["week_start"].apply(_label_row)
plan_edit["week_start"] = plan_edit["week_start"].dt.date

edited = st.data_editor(
    plan_edit,
    column_config={
        "week_start":  st.column_config.DateColumn("Week (Mon)"),
        "weekly_km":   st.column_config.NumberColumn("Weekly km", min_value=0, max_value=200, step=1.0),
        "long_run_km": st.column_config.NumberColumn("Long-run km", min_value=0, max_value=80, step=1.0),
        "notes":       st.column_config.TextColumn("Notes"),
        "race":        st.column_config.TextColumn("Race", disabled=True),
    },
    width="stretch",
    hide_index=True,
    num_rows="dynamic",
    key="plan_editor",
)

save_col, _ = st.columns([1, 6])
if save_col.button("💾 Save plan", type="primary"):
    conn.execute("DELETE FROM race_plan")
    for _, r in edited.iterrows():
        if pd.isna(r["week_start"]):
            continue
        conn.execute(
            """INSERT INTO race_plan (week_start, weekly_km, long_run_km, notes, updated_at)
               VALUES (?, ?, ?, ?, datetime('now'))""",
            (
                pd.Timestamp(r["week_start"]).date().isoformat(),
                float(r["weekly_km"]) if pd.notna(r["weekly_km"]) else None,
                float(r["long_run_km"]) if pd.notna(r["long_run_km"]) else None,
                (r["notes"] or "").strip() or None,
            ),
        )
    conn.commit()
    st.cache_data.clear()
    st.success(f"Saved {len(edited)} plan rows.")


# ---------------------------------------------------------------------------
# Projected PMC
# ---------------------------------------------------------------------------

st.subheader("Projected PMC")

history = daily_training_load_series(conn, include_manual=True)
plan_for_forecast = edited.dropna(subset=["week_start"]).copy()
plan_for_forecast["week_start"] = pd.to_datetime(plan_for_forecast["week_start"])
race_dates_tuple = tuple(d for _, d in cfg.races)
forecast = plan_to_daily_load(
    plan_for_forecast,
    tl_per_km=tl_per_km,
    race_day_tl_per_km=race_tl,
    race_dates=race_dates_tuple,
)

if forecast.empty:
    st.info("Add rows to the plan above to see a projection.")
else:
    today = pd.Timestamp.today().normalize()
    pmc = banister_forecast(history, forecast, today=today)
    if pmc.empty:
        st.info("No PMC to plot — need either history or plan rows.")
    else:
        plot_pmc = pmc.copy()
        fig, ax1 = plt.subplots(figsize=(12, 4.6))
        ax1.plot(plot_pmc.index, plot_pmc["CTL"], color="#2a9d8f", lw=2.2, label="CTL")
        ax1.plot(plot_pmc.index, plot_pmc["ATL"], color="#e76f51", lw=1.2, alpha=0.85, label="ATL")
        ax1.axvline(today, color="black", lw=0.7, ls=":")
        ax1.text(today, ax1.get_ylim()[1] * 0.97, " today ", fontsize=8, va="top")
        ax1.set_ylabel("CTL / ATL")
        ax1.legend(loc="upper left", fontsize=8)
        ax2 = ax1.twinx()
        ax2.fill_between(plot_pmc.index, plot_pmc["TSB"], 0,
                         where=plot_pmc["TSB"] >= 0, color="#2a9d8f", alpha=0.12, interpolate=True)
        ax2.fill_between(plot_pmc.index, plot_pmc["TSB"], 0,
                         where=plot_pmc["TSB"] < 0, color="#e76f51", alpha=0.12, 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")

        # Mark race dates with annotations.
        for label, d in cfg.races:
            rd = pd.Timestamp(d).normalize()
            if rd in plot_pmc.index:
                tsb = plot_pmc.loc[rd, "TSB"]
                ax1.axvline(rd, color="#264653", alpha=0.4, lw=1.0)
                ax1.text(rd, ax1.get_ylim()[1] * 0.85,
                         f"  {label}\n  TSB {tsb:+.0f}",
                         fontsize=8, alpha=0.85)
        fig.tight_layout()
        st.pyplot(fig, clear_figure=True)

        # Race-day TSB table — the actionable summary.
        if cfg.races:
            race_rows = []
            for label, d in cfg.races:
                rd = pd.Timestamp(d).normalize()
                if rd in plot_pmc.index:
                    r = plot_pmc.loc[rd]
                    tsb = r["TSB"]
                    if tsb < -10:
                        tag = "🟠 fatigued"
                    elif tsb < 10:
                        tag = "🟡 building"
                    elif tsb <= 25:
                        tag = "🟢 race-ready"
                    else:
                        tag = "🔵 detrained"
                    race_rows.append({
                        "Race": label,
                        "Date": d,
                        "CTL": round(r["CTL"], 1),
                        "ATL": round(r["ATL"], 1),
                        "TSB": round(tsb, 1),
                        "Form": tag,
                    })
            if race_rows:
                st.subheader("Race-day projection")
                st.dataframe(pd.DataFrame(race_rows), hide_index=True, width="stretch")


# ---------------------------------------------------------------------------
# Fueling plan
# ---------------------------------------------------------------------------

st.subheader("Fueling")
st.caption(
    "Race-experience finding (NatesNotes.md): historically undernourished — "
    "plan carbs ahead, don't wing it. Default target **80 g/h**."
)


def _race_km(label: str) -> float | None:
    """Pull a distance out of a race label like 'wk 4 — 30K' or '50 MILE'."""
    m = re.search(r"(\d+(?:\.\d+)?)\s*(mile|mi\b|k\b)", label, re.IGNORECASE)
    if not m:
        return None
    val = float(m.group(1))
    return val * 1.60934 if m.group(2).lower().startswith("m") else val


if cfg.races:
    carb_rate = st.number_input(
        "Carb target (g/h)", min_value=30, max_value=120, value=80, step=5,
        help="60–90 g/h is the trained-gut range for ultras; 80 is the plan.",
    )
    fuel_cols = st.columns(len(cfg.races))
    for i, (label, d) in enumerate(cfg.races):
        km = _race_km(label)
        # Conservative trail-ultra default pace (7.5 km/h); edit per race.
        default_h = round((km / 7.5) * 2) / 2 if km else 4.0
        with fuel_cols[i]:
            hours = st.number_input(
                f"{label} ({d}) — est. finish (h)",
                min_value=1.0, max_value=24.0, value=float(default_h), step=0.5,
                key=f"fuel_hours_{i}",
            )
            total = carb_rate * hours
            st.metric(label, f"{total:.0f} g carbs",
                      f"≈ {total / 25:.0f} × 25 g gels over {hours:.1f} h",
                      delta_color="off")
else:
    st.info("Add races to openrun.toml to plan fueling.")
