"""Efficiency — m/beat trends, distance-bucket comparison, year-over-year.

Port of notebook 04. The headline metric is m/beat (metres covered per
heartbeat at the run's effort): `distance_m / (duration_s * avg_hr / 60)`.
Higher = more efficient aerobic system. Best compared YoY within a tight
distance bucket so terrain and intent don't dominate.

Layout:
    Row 1: latest 30/90/365-day m/beat with delta vs prior period.
    Row 2: m/beat over time + rolling 30-run median.
    Row 3: distance-bucket × year heatmap (median m/beat per cell).
    Row 4: easy-runs-only headline view (HR < 75% of LTHR).
"""

from __future__ import annotations

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

from openrun import default_profile
from openrun.web._helpers import cached_activities, empty_state, show_sidebar


st.set_page_config(page_title="Efficiency · openrun", page_icon="🫀", layout="wide")
show_sidebar()
st.title("🫀 Efficiency — metres per heartbeat")
st.caption(
    "How much ground you cover per heartbeat at the effort you ran. "
    "Higher = more efficient aerobic system; meaningful YoY when distance + terrain are similar."
)

runs = cached_activities(type="running")
if runs.empty:
    empty_state("No running activities yet.")
    st.stop()

# Compute m/beat per run; mask the implausible ones the same way load_activities masks pace.
df = runs.dropna(subset=["distance_m", "duration_s", "avg_hr"]).copy()
df["m_per_beat"] = df["distance_m"] / (df["duration_s"] * df["avg_hr"] / 60.0)
df = df[(df["m_per_beat"] > 1.0) & (df["m_per_beat"] < 8.0)]  # 1-8 m/beat covers walks→elites
df["year"] = df["start_time_local"].dt.year
df["month"] = df["start_time_local"].dt.to_period("M").dt.start_time

if df.empty:
    empty_state("No runs with both `avg_hr` and `distance` populated.")
    st.stop()


# ---------------------------------------------------------------------------
# Row 1 — period tiles with delta
# ---------------------------------------------------------------------------

def _period_median(days: int) -> tuple[float | None, float | None]:
    now = pd.Timestamp.now("UTC").tz_localize(None)
    cur = df[df["start_time_local"] >= (now - pd.Timedelta(days=days))]
    prev = df[
        (df["start_time_local"] < (now - pd.Timedelta(days=days)))
        & (df["start_time_local"] >= (now - pd.Timedelta(days=2 * days)))
    ]
    return (
        float(cur["m_per_beat"].median()) if not cur.empty else None,
        float(prev["m_per_beat"].median()) if not prev.empty else None,
    )


cols = st.columns(3)
for col, days, label in zip(cols, (30, 90, 365), ("30d", "90d", "1y")):
    cur, prev = _period_median(days)
    delta = (cur - prev) if (cur is not None and prev is not None) else None
    col.metric(
        f"m/beat · {label}",
        f"{cur:.2f}" if cur is not None else "—",
        f"{delta:+.2f} vs prior {label}" if delta is not None else None,
    )


# ---------------------------------------------------------------------------
# Row 2 — m/beat over time with rolling 30-run median
# ---------------------------------------------------------------------------

st.subheader("Trend")
chrono = df.sort_values("start_time_local").set_index("start_time_local")
fig, ax = plt.subplots(figsize=(11, 3.6))
ax.scatter(chrono.index, chrono["m_per_beat"], alpha=0.35, s=12, color="#264653", label="run")
roll = chrono["m_per_beat"].rolling(30, min_periods=10).median()
ax.plot(roll.index, roll, color="#e76f51", lw=2.2, label="30-run median")
ax.set_ylabel("m/beat")
ax.legend(loc="upper left", fontsize=8)
fig.tight_layout()
st.pyplot(fig, clear_figure=True)


# ---------------------------------------------------------------------------
# Row 3 — distance bucket × year
# ---------------------------------------------------------------------------

st.subheader("Distance bucket × year")
st.caption("Median m/beat per cell. Empty cells = no runs in that bucket-year.")

bins = [0, 5, 8, 12, 18, 26, 35, 50, 100]
labels = ["<5", "5–8", "8–12", "12–18", "18–26", "26–35", "35–50", "50+"]
df["bucket"] = pd.cut(df["distance_km"], bins=bins, labels=labels, right=False)
pivot = df.pivot_table(
    index="bucket", columns="year", values="m_per_beat", aggfunc="median", observed=True
)

if not pivot.empty:
    fig, ax = plt.subplots(figsize=(8, 3.5))
    im = ax.imshow(pivot.values, aspect="auto", cmap="viridis")
    ax.set_xticks(range(len(pivot.columns)))
    ax.set_xticklabels(pivot.columns)
    ax.set_yticks(range(len(pivot.index)))
    ax.set_yticklabels(pivot.index)
    ax.set_xlabel("year")
    ax.set_ylabel("distance bucket (km)")
    fig.colorbar(im, ax=ax, label="m/beat")
    # Annotate
    for i in range(pivot.shape[0]):
        for j in range(pivot.shape[1]):
            v = pivot.values[i, j]
            if not pd.isna(v):
                ax.text(j, i, f"{v:.2f}", ha="center", va="center",
                        color="white" if v < pivot.values.mean() else "black", fontsize=8)
    fig.tight_layout()
    st.pyplot(fig, clear_figure=True)


# ---------------------------------------------------------------------------
# Row 4 — easy-runs-only headline
# ---------------------------------------------------------------------------

st.subheader("Easy runs only (HR < 75 % of LTHR)")
profile = default_profile()
lthr = profile.lthr or int(profile.hr_max * 0.85) if profile.hr_max else 170
easy_threshold = lthr * 0.75
st.caption(
    f"Filter: avg_hr < {easy_threshold:.0f} bpm. Removes hard sessions where pace at "
    f"high HR confounds the m/beat reading. LTHR pulled from your profile = {lthr}."
)

easy = df[df["avg_hr"] < easy_threshold].copy()
if easy.empty:
    st.info("No runs match the easy filter — adjust your LTHR in openrun.toml or try a wider window.")
else:
    easy_monthly = easy.groupby("month")["m_per_beat"].median().tail(36)
    fig, ax = plt.subplots(figsize=(11, 3.0))
    ax.plot(easy_monthly.index, easy_monthly.values, color="#2a9d8f", lw=2.2, marker="o")
    ax.set_ylabel("median m/beat (easy runs)")
    fig.tight_layout()
    st.pyplot(fig, clear_figure=True)
