"""Sync — Garmin live pull + in-browser export ingest.

Three things, top to bottom:
  1. Status — what state the DB is in (last sync / last ingest).
  2. Live sync — log in to Garmin (email/password, with an MFA round-trip when
     the account needs one) and pull activities, per-second FIT, and wellness
     in-process. Auth state lives in `.secrets/`; the MFA handshake is held in
     `st.session_state` between the password and code steps.
  3. Export ingest — upload a `connect.zip` or point at an unzipped folder.
"""

from __future__ import annotations

import shutil
import tempfile
from pathlib import Path

import streamlit as st

from openrun.config import default_config
from openrun.db import get_state
from openrun.ingest import garminconnect_backend as gauth
from openrun.ingest.garmin_api import run_sync
from openrun.ingest.garmin_export import ingest_path
from openrun.web._helpers import get_conn, show_sidebar


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

cfg = default_config()
conn = get_conn()

st.subheader("Status")
st.write(
    {
        "DB path": str(cfg.db_path),
        "Last live sync (UTC)": get_state(conn, "last_sync_utc") or "(never)",
        "Last export ingest (UTC)": get_state(conn, "last_ingest_utc") or "(never)",
        "Last ingest source": get_state(conn, "last_ingest_source") or "(none)",
    }
)

st.divider()
st.subheader("Live sync (Garmin Connect)")

# Resume validates tokens against the API (one round-trip), so cache the
# result for the browser session instead of re-checking on every rerun.
if "garmin_client" not in st.session_state:
    st.session_state["garmin_client"] = gauth.resume()
_gc = st.session_state["garmin_client"]

if _gc is not None:
    st.success(f"Logged in as **{gauth.user_label(_gc)}**.")

    c1, c2 = st.columns(2)
    full = c1.checkbox(
        "Full backfill",
        value=False,
        help="Re-scan all activities and pull 365 days of wellness (slow). "
             "Leave off for a fast incremental top-up.",
    )
    want_fit = c2.checkbox(
        "Download per-second FIT for new activities",
        value=True,
        help="Pulls each new activity's original FIT so decoupling, FIT time-in-zone, "
             "and the route map work without a website export.",
    )

    fit_backfill = st.checkbox(
        "Also backfill per-second FITs for past activities that are missing them",
        value=False,
    )
    fit_type: str | None = None
    fit_limit: int | None = None
    if fit_backfill:
        b1, b2 = st.columns(2)
        fit_type = (b1.text_input("Type filter", value="running").strip() or None)
        _lim = b2.number_input("Limit (0 = all, newest first)", min_value=0, value=50, step=10)
        fit_limit = int(_lim) or None

    if st.button("🔄 Sync now from Garmin", type="primary"):
        summary = None
        with st.status("Syncing from Garmin…", expanded=True) as status:
            try:
                gauth.patch_garth(_gc)
                summary = run_sync(
                    get_conn(),
                    full=full,
                    fetch_fit=want_fit,
                    fit_backfill=fit_backfill,
                    fit_type=fit_type,
                    fit_limit=fit_limit,
                    progress=status.write,
                )
                status.update(label="Sync complete", state="complete")
            except Exception as exc:  # noqa: BLE001 — surface any API/HTTP error
                status.update(label="Sync failed", state="error")
                st.error(str(exc))
        if summary is not None:
            st.cache_data.clear()
            st.success("Synced from Garmin Connect.")
            st.json(summary)

    with st.expander("Log out"):
        st.caption("Removes the saved OAuth tokens in `.secrets/`. You'll need to log in again to sync.")
        if st.button("Forget saved tokens"):
            shutil.rmtree(gauth.token_dir(), ignore_errors=True)
            st.session_state.pop("garmin_client", None)
            st.rerun()

else:
    st.caption(
        "Log in to pull activities, **per-second FIT data**, and wellness straight from "
        "Garmin — no website export needed. Your credentials go directly to Garmin; only "
        "OAuth tokens are stored locally in `.secrets/`."
    )

    with st.form("garmin_login"):
        email = st.text_input("Garmin email")
        password = st.text_input("Password", type="password")
        submitted = st.form_submit_button("Log in", type="primary")
    if submitted:
        if not email or not password:
            st.warning("Enter both email and password.")
        else:
            try:
                with st.spinner("Logging in to Garmin…"):
                    kind, payload = gauth.begin_login(email, password)
            except Exception as exc:  # noqa: BLE001
                st.error(f"Login failed: {exc}")
            else:
                if kind == "needs_mfa":
                    st.session_state["garmin_mfa_state"] = payload
                    st.rerun()
                else:
                    st.session_state["garmin_client"] = payload
                    st.rerun()

    if "garmin_mfa_state" in st.session_state:
        st.info("Garmin requires a verification code (sent by email or your authenticator app).")
        with st.form("garmin_mfa"):
            code = st.text_input("MFA code")
            mfa_submitted = st.form_submit_button("Verify", type="primary")
        if mfa_submitted:
            try:
                gc = gauth.complete_mfa(st.session_state["garmin_mfa_state"], code)
            except Exception as exc:  # noqa: BLE001
                st.error(f"Verification failed: {exc}")
            else:
                del st.session_state["garmin_mfa_state"]
                st.session_state["garmin_client"] = gc
                st.rerun()

st.divider()
st.subheader("Import a Garmin data export")
st.caption(
    "Upload a `connect.zip` (Connect data export) or point at an unzipped "
    "export folder (Takeout dumps). The ingest runs in-process and updates the DB."
)

mode = st.radio("Source", ["Upload zip", "Path on disk"], horizontal=True, label_visibility="collapsed")

if mode == "Upload zip":
    uploaded = st.file_uploader("Upload connect.zip", type=["zip"])
    cols = st.columns([1, 1, 4])
    dry = cols[1].checkbox("Dry run", value=False, help="Don't write to the DB")
    run = cols[0].button("Ingest", type="primary", disabled=uploaded is None)

    if run and uploaded is not None:
        # Stream the upload to a temp file so ingest_path can handle it like any zip.
        with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
            tmp.write(uploaded.read())
            tmp_path = Path(tmp.name)
        try:
            with st.status(f"Ingesting {uploaded.name}…", expanded=True) as status:
                lines: list[str] = []

                def log(msg: str) -> None:
                    lines.append(msg)
                    status.write(msg)

                summary = ingest_path(tmp_path, conn=get_conn(), dry_run=dry, progress=log)
                status.update(label="Ingest complete", state="complete")
        finally:
            tmp_path.unlink(missing_ok=True)
        st.cache_data.clear()
        st.success(f"Ingested {sum(summary['counts'].values()):,} JSON rows + {summary['fits']} FIT files.")
        st.json(summary)

else:
    folder = st.text_input("Path to an unzipped export folder",
                           placeholder="/path/to/garmin_export/")
    cols = st.columns([1, 1, 4])
    dry = cols[1].checkbox("Dry run", value=False, help="Don't write to the DB", key="dry_folder")
    run = cols[0].button("Ingest", type="primary",
                         disabled=not folder.strip(), key="run_folder")

    if run and folder.strip():
        p = Path(folder).expanduser()
        if not p.exists():
            st.error(f"Path does not exist: {p}")
        else:
            try:
                with st.status(f"Ingesting {p.name}…", expanded=True) as status:
                    def log(msg: str) -> None:
                        status.write(msg)

                    summary = ingest_path(p, conn=get_conn(), dry_run=dry, progress=log)
                    status.update(label="Ingest complete", state="complete")
                st.cache_data.clear()
                st.success(f"Ingested {sum(summary['counts'].values()):,} JSON rows + {summary['fits']} FIT files.")
                st.json(summary)
            except (FileNotFoundError, ValueError) as exc:
                st.error(str(exc))

st.divider()
with st.expander("After ingest: link FITs (Takeout dumps only)"):
    st.markdown(
        """
        Takeout dumps embed *upload IDs* (not activity IDs) in FIT filenames, so the
        ingest above can only link FITs from the Connect-style export. For Takeout,
        run the by-content linker once:

        ```bash
        uv run openrun-link-fit /path/to/export/
        ```

        If you move the export folder later, run with `--relink` to rewrite the
        stored paths.
        """
    )
