"""garminconnect-backed auth for openrun-sync.

garth's password login is Cloudflare-rate-limited (429 on sso.garmin.com) and
garth itself is deprecated. python-garminconnect 0.3.x authenticates with
Garmin's newer DI Bearer tokens (`.secrets/garmin_tokens.json`) and refreshes
them against connectapi.garmin.com — no SSO involved after the first login.

`activate()` logs in via garminconnect, then routes garth's API surface
(module-level `garth.connectapi`/`garth.download`, the `garth.http.client`
singleton used by `garth.stats`, and the `username` property) through the
garminconnect client, so all of `openrun.ingest.garmin_api` runs unchanged.

This is the slim version of ROADMAP P3 (`openrun-sync --backend=garminconnect`).
"""

from __future__ import annotations

from pathlib import Path
from typing import Any


def token_dir() -> Path:
    """Token store: `.secrets/` relative to the current working directory."""
    return Path.cwd() / ".secrets"


def has_tokens(token_dir: Path) -> bool:
    return (token_dir / "garmin_tokens.json").exists()


def resume(td: Path | None = None) -> Any | None:
    """Restore an authenticated Garmin client from saved tokens, else None.

    Validates against the API (refreshing the DI token if needed), so a dead
    tokenstore returns None rather than failing later mid-sync.
    """
    from garminconnect import Garmin

    td = td or token_dir()
    if not has_tokens(td):
        return None
    try:
        gc = Garmin()
        gc.login(tokenstore=str(td))
        return gc
    except Exception:  # noqa: BLE001 — expired/rejected tokens → logged out
        return None


def begin_login(email: str, password: str, td: Path | None = None) -> tuple[str, Any]:
    """Start a fresh credential login (web flow, no input()). Returns:

    * ``("ok", gc)``        — logged in; tokens saved.
    * ``("needs_mfa", gc)`` — hold ``gc`` (e.g. in st.session_state) and pass
                              it with the emailed/app code to ``complete_mfa``.

    Raises garminconnect's typed errors on bad credentials / rate limits.
    """
    from garminconnect import Garmin

    gc = Garmin(email=email, password=password, return_on_mfa=True)
    status, _ = gc.login()  # cascading strategies; early-returns pre-dump
    if status == "needs_mfa":
        return "needs_mfa", gc
    return "ok", _finish_login(gc, td)


def complete_mfa(gc: Any, code: str, td: Path | None = None) -> Any:
    """Finish an MFA login started by ``begin_login``. Returns the client."""
    gc.client.resume_login(None, code.strip())
    return _finish_login(gc, td)


def _finish_login(gc: Any, td: Path | None = None) -> Any:
    """Persist tokens and load the profile (skipped in return_on_mfa mode)."""
    td = td or token_dir()
    td.mkdir(parents=True, exist_ok=True)
    gc.client.dump(str(td))
    gc._load_profile_and_settings()  # noqa: SLF001 — wrapper has no public hook
    return gc


def user_label(gc: Any) -> str:
    """Human-readable account label for UI display."""
    return gc.full_name or gc.username or gc.display_name or "Garmin user"


def activate(token_dir: Path) -> str:
    """Authenticate from `token_dir` and patch garth. Returns the username."""
    from garminconnect import Garmin

    gc = Garmin()
    gc.login(tokenstore=str(token_dir))  # loads DI tokens; auto-refreshes + re-saves
    return patch_garth(gc)


def patch_garth(gc: Any) -> str:
    """Route garth's API surface through an authenticated garminconnect client."""
    import garth
    from garth.http import Client as GarthClient
    from garth.http import client as garth_singleton

    def connectapi(path: str, **kwargs: Any) -> Any:
        return gc.connectapi(path, **kwargs)

    def download(path: str, **kwargs: Any) -> bytes:
        return gc.download(path, **kwargs)

    # Call-time lookups on the singleton (garth.stats.* resolve `http.client`).
    garth_singleton.connectapi = connectapi  # type: ignore[method-assign]
    garth_singleton.download = download  # type: ignore[method-assign]
    # Module-level names were bound to the original methods at import time.
    garth.connectapi = connectapi  # type: ignore[assignment]
    garth.download = download  # type: ignore[assignment]

    # `Client.username` is a read-only property; replace it on the class.
    # sync_resting_hr interpolates it into /usersummary-service/.../{username},
    # which requires the UUID displayName — userName ("o@...") gets a 403.
    profile = gc.connectapi("userprofile-service/socialProfile") or {}
    username = gc.display_name or profile.get("displayName") or profile.get("userName") or ""
    GarthClient.username = username  # type: ignore[assignment]
    return profile.get("userName") or username


def main() -> None:
    """One-time interactive login; saves DI tokens to ./.secrets/garmin_tokens.json.

    Uses garminconnect's cascading login strategies (mobile+cffi first, which
    impersonates a browser TLS fingerprint) — gets past the Cloudflare 429
    that blocks garth's plain SSO login.

        uv run python -m openrun.ingest.garminconnect_backend
    """
    import getpass
    import os

    from garminconnect import Garmin

    td = Path.cwd() / ".secrets"
    td.mkdir(exist_ok=True)

    # A parseable-but-dead tokenstore short-circuits the password fallback in
    # garminconnect's login(); park any existing file so we get a fresh login.
    stale = td / "garmin_tokens.json"
    if stale.exists():
        stale.rename(td / "garmin_tokens.json.bak")
        print(f"(moved old tokens to {stale.name}.bak)")

    email = os.environ.get("GARMIN_EMAIL") or input("Garmin email: ").strip()
    password = os.environ.get("GARMIN_PASSWORD") or getpass.getpass("Garmin password: ")

    gc = Garmin(email=email, password=password,
                prompt_mfa=lambda: input("MFA code: ").strip())
    gc.login(tokenstore=str(td))  # no tokens on disk now → password login, then dump
    print(f"Logged in as {gc.display_name or email}. Tokens saved to {td}.")
    print("Now run:  uv run openrun-sync --days 35")


if __name__ == "__main__":
    main()
