"""Self-service user settings endpoints (/api/me/*)."""
from __future__ import annotations

import json
import shutil
from pathlib import Path
from typing import Any

from fastapi import APIRouter, Cookie, HTTPException, Request
from fastapi.responses import JSONResponse, Response

from bincio.serve import deps, tasks
from bincio.serve.db import (
    authenticate,
    get_user_prefs,
    set_user_prefs,
)

router = APIRouter()


def _wipe_user_activities(user_dir: Path) -> int:
    """Delete all extracted activity files and caches for a user.

    Removes activities/ (JSON + GeoJSON + timeseries), edits/, originals/,
    _merged/, index.json, athlete.json, and the dedup cache.
    Leaves the user directory itself intact (account remains in the DB).
    Returns the number of files deleted.
    """
    import shutil
    deleted = 0

    for subdir in ("activities", "edits", "originals"):
        d = user_dir / subdir
        if d.exists():
            for f in d.rglob("*"):
                if f.is_file():
                    deleted += 1
            shutil.rmtree(d)

    for name in ("_merged", ):
        d = user_dir / name
        if d.exists():
            shutil.rmtree(d)

    for name in ("index.json", "athlete.json", ".bincio_cache.json", "tracks.json", "tracks_index.json"):
        f = user_dir / name
        if f.exists():
            f.unlink()
            deleted += 1

    for shard in user_dir.glob("tracks_*.json"):
        shard.unlink(missing_ok=True)
        deleted += 1

    return deleted


@router.get("/api/me/tracks")
async def me_tracks(bincio_session: str | None = Cookie(default=None)) -> Response:
    """Return the tracks manifest (years list + total) for the logged-in user."""
    user = deps._require_user(bincio_session)
    index_path = deps._get_data_dir() / user.handle / "tracks_index.json"
    if not index_path.exists():
        raise HTTPException(404, "Tracks not yet baked — upload an activity first")
    return Response(content=index_path.read_bytes(), media_type="application/json")


@router.get("/api/me/tracks/{year}")
async def me_tracks_year(year: str, bincio_session: str | None = Cookie(default=None)) -> Response:
    """Return the pre-baked tracks shard for a specific year."""
    user = deps._require_user(bincio_session)
    if not year.isdigit() or len(year) != 4:
        raise HTTPException(400, "year must be a 4-digit string")
    shard_path = deps._get_data_dir() / user.handle / f"tracks_{year}.json"
    if not shard_path.exists():
        raise HTTPException(404, f"No tracks shard for year {year}")
    return Response(content=shard_path.read_bytes(), media_type="application/json")


@router.get("/api/me/storage")
async def me_storage(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
    """Return per-category disk usage for the logged-in user."""
    user = deps._require_user(bincio_session)
    dd = deps._get_data_dir() / user.handle

    def _mb(path: Path) -> float:
        if not path.exists():
            return 0.0
        total = sum(f.lstat().st_size for f in path.rglob("*") if f.is_file() or f.is_symlink())
        return round(total / 1_048_576, 2)

    def _count(path: Path, pattern: str = "*") -> int:
        if not path.exists():
            return 0
        return sum(1 for f in path.glob(pattern) if f.is_file())

    activities_mb   = _mb(dd / "activities")
    originals_mb    = _mb(dd / "originals")
    strava_mb       = _mb(dd / "originals" / "strava")
    images_mb       = _mb(dd / "edits" / "images")
    total_mb        = _mb(dd)

    return JSONResponse({
        "total_mb":          total_mb,
        "activities_mb":     activities_mb,
        "activities_count":  _count(dd / "activities", "*.json"),
        "originals_mb":      originals_mb,
        "strava_originals_mb": strava_mb,
        "strava_originals_count": _count(dd / "originals" / "strava", "*.json"),
        "images_mb":         images_mb,
    })


@router.delete("/api/me/originals")
async def me_delete_originals(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
    """Delete the user's originals/ directory (frees space after re-extraction)."""
    user = deps._require_user(bincio_session)
    originals = deps._get_data_dir() / user.handle / "originals"
    if not originals.exists():
        return JSONResponse({"ok": True, "freed_mb": 0.0})

    freed = round(
        sum(f.stat().st_size for f in originals.rglob("*") if f.is_file()) / 1_048_576, 2
    )
    shutil.rmtree(originals)
    return JSONResponse({"ok": True, "freed_mb": freed})


@router.delete("/api/me/activities")
async def me_delete_activities(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Wipe all extracted activity data (activities/, edits/, _merged/, index/athlete JSON).

    Requires the user's current password in the request body for confirmation.
    """
    user = deps._require_user(bincio_session)
    body = await request.json()
    password = body.get("password", "")
    if not authenticate(deps._get_db(), user.handle, password):
        raise HTTPException(401, "Wrong password")

    user_dir = deps._get_data_dir() / user.handle
    deleted = _wipe_user_activities(user_dir)
    tasks._trigger_rebuild(user.handle)
    return JSONResponse({"ok": True, "deleted": deleted})


@router.delete("/api/me")
async def me_delete_account(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Delete the account and all data permanently.

    Requires the user's current password. Deletes the DB row, all sessions,
    and the entire user data directory. The root shard manifest is updated.
    """
    user = deps._require_user(bincio_session)
    body = await request.json()
    password = body.get("password", "")
    if not authenticate(deps._get_db(), user.handle, password):
        raise HTTPException(401, "Wrong password")

    # Wipe data directory
    user_dir = deps._get_data_dir() / user.handle
    if user_dir.is_dir():
        shutil.rmtree(user_dir)

    # Remove from DB (cascades to sessions, invites, reset_codes)
    from bincio.serve.db import delete_user as _delete_user
    _delete_user(deps._get_db(), user.handle)

    # Update root manifest so the shard disappears
    from bincio.render.cli import _write_root_manifest
    try:
        _write_root_manifest(deps._get_data_dir())
    except (OSError, json.JSONDecodeError):
        pass

    resp = JSONResponse({"ok": True})
    resp.delete_cookie(deps._SESSION_COOKIE)
    return resp


@router.put("/api/me/display-name")
async def me_update_display_name(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Update the logged-in user's display name."""
    user = deps._require_user(bincio_session)
    body = await request.json()
    display_name = str(body.get("display_name", "")).strip()
    if len(display_name) > 60:
        raise HTTPException(400, "Display name too long (max 60 characters)")
    db = deps._get_db()
    db.execute("UPDATE users SET display_name = ? WHERE handle = ?", (display_name, user.handle))
    db.commit()
    return JSONResponse({"ok": True, "display_name": display_name})


@router.get("/api/me/prefs")
async def me_get_prefs(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
    """Return all user preferences as a key→value dict."""
    user = deps._require_user(bincio_session)
    return JSONResponse(get_user_prefs(deps._get_db(), user.handle))


@router.put("/api/me/prefs")
async def me_set_prefs(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Upsert one or more user preferences. Body: {key: value, ...} (all strings)."""
    user = deps._require_user(bincio_session)
    body = await request.json()
    if not isinstance(body, dict):
        raise HTTPException(400, "Body must be a JSON object")
    # Coerce all values to strings; ignore unknown keys silently
    prefs = {str(k): str(v) for k, v in body.items()}
    set_user_prefs(deps._get_db(), user.handle, prefs)

    # Mirror download_disabled_default to a file so the render pipeline can read it
    if "download_disabled_default" in prefs:
        user_dir = deps._get_data_dir() / user.handle
        settings_path = user_dir / "_user_settings.json"
        try:
            current = json.loads(settings_path.read_text(encoding="utf-8")) if settings_path.exists() else {}
        except (OSError, json.JSONDecodeError):
            current = {}
        current["download_disabled_default"] = prefs["download_disabled_default"] == "true"
        settings_path.write_text(json.dumps(current, indent=2), encoding="utf-8")

    return JSONResponse({"ok": True})


@router.get("/api/me/strava-credentials")
async def me_get_strava_credentials(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
    """Return whether per-user Strava credentials are configured (never returns the secret)."""
    user = deps._require_user(bincio_session)
    creds_path = deps._get_data_dir() / user.handle / deps._STRAVA_CREDS_FILE
    has_user_creds = False
    client_id_hint = ""
    if creds_path.exists():
        try:
            d = json.loads(creds_path.read_text(encoding="utf-8"))
            cid = str(d.get("client_id", "")).strip()
            csec = str(d.get("client_secret", "")).strip()
            if cid and csec:
                has_user_creds = True
                client_id_hint = cid
        except (OSError, json.JSONDecodeError):
            pass
    return JSONResponse({
        "has_user_creds": has_user_creds,
        "client_id": client_id_hint,
        "instance_configured": bool(deps.strava_client_id),
    })


@router.put("/api/me/strava-credentials")
async def me_set_strava_credentials(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Save per-user Strava credentials. Body: {client_id, client_secret}."""
    user = deps._require_user(bincio_session)
    body = await request.json()
    cid  = str(body.get("client_id",     "")).strip()
    csec = str(body.get("client_secret", "")).strip()
    if not cid:
        raise HTTPException(400, "client_id is required")
    creds_path = deps._get_data_dir() / user.handle / deps._STRAVA_CREDS_FILE
    # If client_secret is omitted, preserve existing secret (if any)
    if not csec:
        if creds_path.exists():
            try:
                existing = json.loads(creds_path.read_text(encoding="utf-8"))
                csec = str(existing.get("client_secret", "")).strip()
            except (OSError, json.JSONDecodeError):
                pass
    if not csec:
        raise HTTPException(400, "client_secret is required (no existing secret to preserve)")

    # If the client_id changed, the existing token belongs to a different OAuth
    # app and will fail on refresh — delete it so the user must re-authenticate.
    token_path = deps._get_data_dir() / user.handle / "strava_token.json"
    if creds_path.exists() and token_path.exists():
        try:
            old_cid = str(json.loads(creds_path.read_text(encoding="utf-8")).get("client_id", "")).strip()
            if old_cid and old_cid != cid:
                token_path.unlink(missing_ok=True)
        except (OSError, json.JSONDecodeError):
            pass

    creds_path.write_text(
        json.dumps({"client_id": cid, "client_secret": csec}, indent=2),
        encoding="utf-8",
    )
    return JSONResponse({"ok": True})


@router.delete("/api/me/strava-credentials")
async def me_delete_strava_credentials(bincio_session: str | None = Cookie(default=None)) -> JSONResponse:
    """Remove per-user Strava credentials (falls back to instance credentials)."""
    user = deps._require_user(bincio_session)
    creds_path = deps._get_data_dir() / user.handle / deps._STRAVA_CREDS_FILE
    creds_path.unlink(missing_ok=True)
    return JSONResponse({"ok": True})


@router.put("/api/me/password")
async def me_change_password(
    request: Request,
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Change the logged-in user's password. Requires current password."""
    from bincio.serve.db import change_password as _change_password
    user = deps._require_user(bincio_session)
    body = await request.json()
    current  = body.get("current_password", "")
    new_pw   = body.get("new_password", "")
    if not authenticate(deps._get_db(), user.handle, current):
        raise HTTPException(401, "Current password is wrong")
    if len(new_pw) < 8:
        raise HTTPException(400, "New password must be at least 8 characters")
    _change_password(deps._get_db(), user.handle, new_pw)
    return JSONResponse({"ok": True})


@router.get("/api/me/sync-status")
async def get_sync_status(
    bincio_session: str | None = Cookie(default=None),
) -> JSONResponse:
    """Return the last sync status for Strava and Garmin for the logged-in user."""
    user = deps._require_user(bincio_session)
    user_dir = deps._get_data_dir() / user.handle

    def _read_status(filename: str) -> dict | None:
        p = user_dir / filename
        try:
            return json.loads(p.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            return None

    strava = _read_status("_strava_sync_status.json")
    garmin = _read_status("_garmin_sync_status.json")
    return JSONResponse({
        "strava": strava,
        "garmin": garmin,
    })
