"""Strava integration endpoints (/api/strava/*)."""
from __future__ import annotations

import json
import secrets
from typing import Optional

from fastapi import APIRouter, Cookie, HTTPException, Request
from fastapi.responses import JSONResponse, RedirectResponse, StreamingResponse

from bincio.serve import deps, tasks
from bincio.serve.db import get_setting

router = APIRouter()

_strava_oauth_states: set[str] = set()


@router.get("/api/strava/status")
async def strava_status(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    user = deps._require_user(bincio_session)
    cid, _ = deps._strava_creds(user.handle)
    if not cid:
        return JSONResponse({"configured": False, "connected": False, "last_sync": None})
    dd = deps._get_data_dir() / user.handle
    from bincio.extract.strava_api import load_token
    token = load_token(dd)
    return JSONResponse({
        "configured": True,
        "connected": token is not None,
        "last_sync": token.get("last_sync_at") if token else None,
    })


@router.post("/api/strava/disconnect")
async def strava_disconnect(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    """Remove the stored Strava token, forcing a fresh OAuth on next connect."""
    user = deps._require_user(bincio_session)
    token_path = deps._get_data_dir() / user.handle / "strava_token.json"
    token_path.unlink(missing_ok=True)
    return JSONResponse({"ok": True})


@router.post("/api/strava/reset")
async def strava_reset(request: Request, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    """Reset last_sync_at so the next sync re-fetches from a chosen point.

    mode=soft  — set to the started_at of the most recent activity on disk
                 (next sync only fetches activities newer than the last known one)
    mode=hard  — clear last_sync_at entirely
                 (next sync re-downloads full Strava history, skipping existing files)
    """
    user = deps._require_user(bincio_session)
    dd = deps._get_data_dir() / user.handle
    from bincio.extract.strava_api import load_token, save_token
    token = load_token(dd)
    if token is None:
        raise HTTPException(400, "Not connected to Strava")

    body = await request.json()
    mode = body.get("mode", "soft")

    if mode == "hard":
        token.pop("last_sync_at", None)
        save_token(dd, token)
        return JSONResponse({"ok": True, "mode": "hard", "last_sync_at": None})

    # soft: find the most recent started_at across the user's merged index
    from datetime import datetime, timezone
    last_ts: int | None = None
    for index_path in [dd / "_merged" / "index.json", dd / "index.json"]:
        if not index_path.exists():
            continue
        try:
            index_data = json.loads(index_path.read_text(encoding="utf-8"))
            started_ats = [
                a.get("started_at") for a in index_data.get("activities", [])
                if a.get("started_at")
            ]
            if started_ats:
                latest = max(started_ats)
                dt = datetime.fromisoformat(latest.replace("Z", "+00:00"))
                last_ts = int(dt.astimezone(timezone.utc).timestamp())
                break
        except (OSError, json.JSONDecodeError, ValueError):
            continue

    if last_ts is None:
        token.pop("last_sync_at", None)
    else:
        token["last_sync_at"] = last_ts
    save_token(dd, token)
    return JSONResponse({"ok": True, "mode": "soft", "last_sync_at": last_ts})


@router.get("/api/strava/auth-url")
async def strava_auth_url(request: Request, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    user = deps._require_user(bincio_session)
    cid, _ = deps._strava_creds(user.handle)
    if not cid:
        raise HTTPException(400, "Strava client ID not configured on this server")
    state = secrets.token_urlsafe(16)
    _strava_oauth_states.add(state)
    if deps.public_url:
        redirect_uri = deps.public_url.rstrip("/") + "/api/strava/callback"
    else:
        redirect_uri = str(request.url_for("strava_callback"))
    from bincio.extract.strava_api import auth_url
    return JSONResponse({"url": auth_url(cid, redirect_uri, state=state)})


@router.get("/api/strava/callback", name="strava_callback")
async def strava_callback(
    request: Request,
    code: str = "",
    error: str = "",
    state: str = "",
    bincio_session: Optional[str] = Cookie(default=None),
) -> RedirectResponse:
    site_origin = deps.public_url.rstrip("/") if deps.public_url else str(request.base_url).rstrip("/")
    if error or not code:
        return RedirectResponse(f"{site_origin}/?strava=error")
    if state not in _strava_oauth_states:
        return RedirectResponse(f"{site_origin}/?strava=error")
    _strava_oauth_states.discard(state)
    user = deps._current_user(bincio_session)
    if not user:
        return RedirectResponse(f"{site_origin}/?strava=error")
    cid, csec = deps._strava_creds(user.handle)
    if not cid or not csec:
        return RedirectResponse(f"{site_origin}/?strava=error")
    dd = deps._get_data_dir() / user.handle
    from bincio.extract.strava_api import StravaError, exchange_code, save_token
    try:
        token = exchange_code(cid, csec, code)
    except StravaError:
        return RedirectResponse(f"{site_origin}/?strava=error")
    save_token(dd, token)
    return RedirectResponse(f"{site_origin}/?strava=connected")


@router.get("/api/strava/sync/stream")
async def serve_strava_sync_stream(bincio_session: Optional[str] = Cookie(default=None)) -> StreamingResponse:
    """SSE endpoint — streams per-activity progress then a final summary event."""
    user = deps._require_user(bincio_session)
    cid, csec = deps._strava_creds(user.handle)
    if not cid or not csec:
        raise HTTPException(400, "Strava not configured on this server")
    dd = deps._get_data_dir() / user.handle
    store_orig_setting = get_setting(deps._get_db(), "store_originals")
    store_orig = store_orig_setting == "true"
    originals_dir = (dd / "originals" / "strava") if store_orig else None
    if originals_dir:
        originals_dir.mkdir(parents=True, exist_ok=True)

    from bincio.extract.ingest import strava_sync_iter

    def event_stream():
        try:
            for event in strava_sync_iter(dd, cid, csec, originals_dir):
                if event["type"] == "done":
                    tasks._trigger_rebuild(user.handle)  # start before client closes connection
                yield f"data: {json.dumps(event)}\n\n"
        except Exception as exc:
            yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"

    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )


@router.post("/api/strava/import-gear")
async def serve_strava_import_gear(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    """One-time backfill: scan stored Strava originals for gear_ids, fetch names, populate gear registry."""
    user = deps._require_user(bincio_session)
    cid, csec = deps._strava_creds(user.handle)
    if not cid or not csec:
        raise HTTPException(400, "Strava not configured on this server")
    dd = deps._get_data_dir() / user.handle
    originals_dir = dd / "originals" / "strava"
    if not originals_dir.exists():
        return JSONResponse({"ok": True, "gear_added": 0, "activities_updated": 0, "message": "No stored originals found"})

    import contextlib
    import uuid

    from bincio.extract.strava_api import StravaError, ensure_fresh, fetch_gear
    from bincio.render.merge import merge_one
    from bincio.serve.routers.gear import _load as _gear_load, _save as _gear_save

    try:
        token = ensure_fresh(dd, cid, csec)
    except StravaError as e:
        raise HTTPException(502, str(e))

    registry = _gear_load(dd)
    known_strava_ids = {g.get("strava_id") for g in registry if g.get("strava_id")}

    # Collect all unique gear_ids from originals
    gear_id_to_activities: dict[str, list[str]] = {}
    for orig_path in originals_dir.glob("*.json"):
        try:
            data = json.loads(orig_path.read_text(encoding="utf-8"))
            gear_id = (data.get("meta") or {}).get("gear_id") or ""
            if gear_id:
                gear_id_to_activities.setdefault(gear_id, []).append(orig_path.stem)
        except (OSError, json.JSONDecodeError):
            continue

    gear_added = 0
    activities_updated = 0

    for gear_id, activity_ids in gear_id_to_activities.items():
        if gear_id in known_strava_ids:
            gear_name = next(g["name"] for g in registry if g.get("strava_id") == gear_id)
        else:
            details = fetch_gear(token["access_token"], gear_id)
            gear_name = details.get("name") or ""
            if not gear_name:
                continue
            gear_type = "shoes" if gear_id.startswith("g") else "bike"
            new_item: dict = {"id": str(uuid.uuid4()), "name": gear_name, "type": gear_type, "retired": False, "strava_id": gear_id}
            registry.append(new_item)
            known_strava_ids.add(gear_id)
            gear_added += 1

        # Backfill: write sidecar for each activity that has no gear set yet
        import yaml as _yaml
        edits_dir = dd / "edits"
        edits_dir.mkdir(exist_ok=True)
        for activity_id in activity_ids:
            activity_json = dd / "activities" / f"{activity_id}.json"
            if not activity_json.exists():
                continue
            try:
                act = json.loads(activity_json.read_text(encoding="utf-8"))
                if act.get("gear"):
                    continue  # already has gear
            except (OSError, json.JSONDecodeError):
                continue
            sidecar = edits_dir / f"{activity_id}.md"
            fm: dict = {}
            body = ""
            if sidecar.exists():
                try:
                    text = sidecar.read_text(encoding="utf-8")
                    import re as _re
                    parts = _re.split(r"^---[ \t]*$", text, maxsplit=2, flags=_re.MULTILINE)
                    if len(parts) >= 3:
                        fm = _yaml.safe_load(parts[1]) or {}
                        body = parts[2].strip()
                except Exception:
                    pass
                if fm.get("gear"):
                    continue  # sidecar already sets gear
            fm["gear"] = gear_name
            fm_text = _yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True).strip()
            content = f"---\n{fm_text}\n---\n"
            if body:
                content += f"\n{body}\n"
            sidecar.write_text(content, encoding="utf-8")
            with contextlib.suppress(Exception):
                merge_one(dd, activity_id)
            activities_updated += 1

    _gear_save(dd, registry)
    tasks._trigger_rebuild(user.handle)
    return JSONResponse({"ok": True, "gear_added": gear_added, "activities_updated": activities_updated})


@router.post("/api/strava/sync")
async def serve_strava_sync(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
    user = deps._require_user(bincio_session)
    cid, csec = deps._strava_creds(user.handle)
    if not cid or not csec:
        raise HTTPException(400, "Strava not configured on this server")
    dd = deps._get_data_dir() / user.handle
    store_orig_setting = get_setting(deps._get_db(), "store_originals")
    store_orig = store_orig_setting == "true"
    originals_dir = (dd / "originals" / "strava") if store_orig else None
    if originals_dir:
        originals_dir.mkdir(parents=True, exist_ok=True)
    from bincio.edit.ops import run_strava_sync
    try:
        result = run_strava_sync(dd, cid, csec, originals_dir=originals_dir)
    except RuntimeError as e:
        raise HTTPException(502, str(e))
    tasks._trigger_rebuild(user.handle)
    return JSONResponse(result)
