"""Shared helpers for the Streamlit pages.

Pages should depend on these wrappers rather than importing model loaders
directly so caching, error surfacing, and "no data yet" handling stay in one
place.
"""

from __future__ import annotations

import sqlite3
from datetime import datetime, timedelta

import pandas as pd
import streamlit as st

from openrun import (
    banister,
    calibrate_tl_per_km,
    daily_training_load_series,
    load_activities,
    load_sleep_stages,
    load_wellness,
    open_conn,
    personal_records,
    weekly_time_in_zone,
)
from openrun.config import default_config
from openrun.db import get_state


@st.cache_resource
def get_conn() -> sqlite3.Connection:
    """Module-singleton SQLite connection — Streamlit reruns the script on
    every interaction, so a fresh `connect()` each time would hammer the DB.

    `check_same_thread=False` because Streamlit's AppTest framework and page
    reruns each spin up their own thread; SQLite under WAL mode handles
    concurrent reads safely, and we only write from one place at a time.
    """
    from openrun.db import connect
    return connect(check_same_thread=False)


@st.cache_data(ttl=60)
def cached_activities(type: str | None = "running") -> pd.DataFrame:
    return load_activities(get_conn(), type=type)


@st.cache_data(ttl=60)
def cached_wellness() -> pd.DataFrame:
    return load_wellness(get_conn())


@st.cache_data(ttl=60)
def cached_sleep_stages() -> pd.DataFrame:
    return load_sleep_stages(get_conn())


@st.cache_data(ttl=60)
def cached_pmc(include_manual: bool = True) -> pd.DataFrame:
    return banister(daily_training_load_series(get_conn(), include_manual=include_manual))


@st.cache_data(ttl=60)
def cached_weekly_tiz(weeks: int = 12) -> pd.DataFrame:
    end = pd.Timestamp.utcnow().normalize()
    start = end - pd.Timedelta(weeks=weeks)
    return weekly_time_in_zone(get_conn(), start=start, end=end)


@st.cache_data(ttl=60)
def cached_personal_records() -> pd.DataFrame:
    return personal_records(cached_activities(type="running"))


@st.cache_data(ttl=300)
def cached_tl_per_km() -> dict:
    return calibrate_tl_per_km(get_conn())


def last_sync_label() -> str:
    """Human-readable last-sync / last-ingest line for the sidebar."""
    conn = get_conn()
    sync = get_state(conn, "last_sync_utc")
    ingest = get_state(conn, "last_ingest_utc")
    parts: list[str] = []
    if sync:
        parts.append(f"sync · {_relative(sync)}")
    if ingest:
        parts.append(f"ingest · {_relative(ingest)}")
    return " | ".join(parts) if parts else "no sync yet"


def _relative(iso_ts: str) -> str:
    try:
        ts = datetime.fromisoformat(iso_ts.replace("Z", ""))
    except ValueError:
        return iso_ts
    delta = datetime.utcnow() - ts
    if delta < timedelta(minutes=1):
        return "just now"
    if delta < timedelta(hours=1):
        return f"{int(delta.total_seconds() // 60)}m ago"
    if delta < timedelta(days=1):
        return f"{int(delta.total_seconds() // 3600)}h ago"
    if delta < timedelta(days=30):
        return f"{delta.days}d ago"
    return ts.date().isoformat()


def show_sidebar() -> None:
    """Sidebar chrome rendered on every page — last sync + DB path + nav hint."""
    cfg = default_config()
    with st.sidebar:
        st.markdown("### openrun")
        st.caption("local-first endurance analytics")
        st.markdown("---")
        st.caption(f"**Last activity:** {last_sync_label()}")
        st.caption(f"**DB:** `{cfg.db_path}`")


def empty_state(message: str, hint: str | None = None) -> None:
    """Standard 'no data yet' panel — use when a page would otherwise be blank."""
    st.info(message)
    if hint:
        st.caption(hint)
