"""Headless multi-user Garmin sync — designed to run as a systemd timer.

For each user directory that contains garmin_creds.json, tries to refresh
the cached garth OAuth2 session (fast, no full login), falls back to a full
email/password re-login only when the session has expired, then fetches and
ingests new activities via garmin_sync_iter.

After all users are synced, optionally POSTs to a server endpoint to trigger
an Astro rebuild + rsync.
"""
from __future__ import annotations

import json
import logging
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

import click

_STATUS_FILE = "_garmin_sync_status.json"

log = logging.getLogger("bincio.sync_garmin")


def _write_status(
    user_dir: Path,
    status: str,
    imported: int,
    errors: int,
    error_message: str | None = None,
) -> None:
    payload: dict = {
        "status": status,
        "imported": imported,
        "errors": errors,
        "last_run": datetime.now(timezone.utc).isoformat(),
    }
    if error_message is not None:
        payload["error_message"] = error_message
    try:
        (user_dir / _STATUS_FILE).write_text(
            json.dumps(payload, indent=2), encoding="utf-8"
        )
    except Exception:
        pass


def _post_rebuild(url: str, secret: str | None) -> None:
    headers: dict[str, str] = {"Content-Type": "application/json"}
    if secret:
        headers["X-Sync-Secret"] = secret
    req = urllib.request.Request(url, data=b"{}", headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            log.info("rebuild triggered: HTTP %d", resp.status)
    except urllib.error.HTTPError as exc:
        log.error("rebuild trigger failed: HTTP %d %s", exc.code, exc.read().decode()[:100])
    except Exception as exc:
        log.error("rebuild trigger failed: %s", exc)


def sync_user(data_dir: Path, user_dir: Path) -> tuple[int, int]:
    """Sync one user's Garmin activities.

    Returns (imported_count, error_count). Skips silently if no credentials.
    """
    from bincio.extract.garmin_api import GarminError, has_credentials, get_client
    from bincio.extract.garmin_sync import run_garmin_sync

    handle = user_dir.name

    if not has_credentials(user_dir):
        log.debug("sync[%s]: no garmin_creds.json — skipped", handle)
        _write_status(user_dir, "no_credentials", 0, 0)
        return 0, 0

    # Explicit auth step so we can distinguish auth failures from API failures.
    try:
        get_client(data_dir, user_dir)
    except GarminError as exc:
        log.error("sync[%s]: auth failed: %s", handle, exc)
        _write_status(user_dir, "auth_error", 0, 1, str(exc))
        return 0, 1

    try:
        result = run_garmin_sync(data_dir, user_dir)
    except RuntimeError as exc:
        log.error("sync[%s]: sync failed: %s", handle, exc)
        _write_status(user_dir, "api_error", 0, 1, str(exc))
        return 0, 1

    imported    = result.get("imported", 0)
    error_count = result.get("error_count", 0)

    log.info("sync[%s]: done — %d imported, %d errors", handle, imported, error_count)
    _write_status(user_dir, "ok", imported, error_count)
    return imported, error_count


def sync_all(root_data_dir: Path) -> dict[str, tuple[int, int]]:
    """Sync all users that have garmin_creds.json. Returns {handle: (imported, errors)}."""
    results: dict[str, tuple[int, int]] = {}
    cred_files = sorted(root_data_dir.glob("*/garmin_creds.json"))
    if not cred_files:
        log.info("sync_all: no users with garmin_creds.json found in %s", root_data_dir)
        return results
    log.info("sync_all: %d user(s) with Garmin credentials", len(cred_files))
    for cf in cred_files:
        user_dir = cf.parent
        handle   = user_dir.name
        try:
            results[handle] = sync_user(root_data_dir, user_dir)
        except Exception as exc:
            log.exception("sync_all[%s]: unexpected error: %s", handle, exc)
            results[handle] = (0, -1)
    return results


@click.command("sync-garmin")
@click.option("--data-dir", "data_dir_str", required=True,
              help="Root data dir (parent of all user dirs, e.g. /var/bincio/data).")
@click.option("--user", "only_user", default=None,
              help="Sync only this handle instead of all users.")
@click.option("--rebuild-url", default=None, envvar="BINCIO_REBUILD_URL",
              help="POST here after a successful sync to trigger a site rebuild.")
@click.option("--rebuild-secret", default=None, envvar="BINCIO_SYNC_SECRET",
              help="Value sent as X-Sync-Secret header to the rebuild endpoint.")
def sync_garmin_cmd(
    data_dir_str: str,
    only_user: str | None,
    rebuild_url: str | None,
    rebuild_secret: str | None,
) -> None:
    """Headless Garmin sync for all users (designed for systemd timer).

    Discovers every user directory that has garmin_creds.json, tries to
    resume the cached garth session (no full re-login if the token is still
    valid), fetches new activities, and optionally triggers a site rebuild.
    """
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s — %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )

    root = Path(data_dir_str).expanduser().resolve()
    if not root.is_dir():
        raise click.ClickException(f"Data dir not found: {root}")

    if only_user:
        user_dir = root / only_user
        if not user_dir.is_dir():
            raise click.ClickException(f"User dir not found: {user_dir}")
        new_count, err_count = sync_user(root, user_dir)
        click.echo(f"{only_user}: {new_count} imported, {err_count} errors")
        total_new = new_count
    else:
        results  = sync_all(root)
        total_new = sum(n for n, _ in results.values())
        total_err = sum(e for _, e in results.values())
        click.echo(
            f"Sync complete: {len(results)} users, "
            f"{total_new} new activities, {total_err} errors"
        )

    if total_new > 0 and rebuild_url:
        _post_rebuild(rebuild_url, rebuild_secret)
