"""Connecting a user's Spotify account.

Two browser-facing routes under `/spotify/` (they redirect, so they must be
full-page navigations, not fetches) and two JSON routes under `/api/spotify/`.

Both prefixes are GATED by AuthMiddleware — connecting Spotify is something only
a signed-in, invited user does. The callback is reached by a top-level GET
redirect from Spotify, which under SameSite=Lax still carries the app session
cookie, so it stays inside the gate too.

Sync `def`: these make blocking server-to-server calls to Spotify plus a DB
write. FastAPI runs sync handlers in a threadpool, which is correct for blocking
work — `async` here would block the event loop (react.md).
"""

from __future__ import annotations

import logging
import secrets

from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session
from starlette.responses import RedirectResponse

from ..app_session import COOKIE_KW
from ..config import get_settings
from ..db import get_db
from ..deps import current_user
from ..errors import error_response
from ..models.identity import User
from ..services import spotify
from ..services.crypto import TokenCryptoError

log = logging.getLogger(__name__)

browser_router = APIRouter(prefix="/spotify", tags=["spotify"])
api_router = APIRouter(prefix="/api/spotify", tags=["spotify"])

# Anti-CSRF for the OAuth round-trip: a random value set as an HttpOnly cookie
# and compared against the `state` Spotify echoes back.
STATE_COOKIE = "spotify_oauth_state"
STATE_MAX_AGE = 600

# Where the browser lands after the round-trip. A query flag rather than a
# server-rendered page, because the SPA owns every screen.
DONE_URL = "/?spotify=connected"
FAILED_URL = "/?spotify=failed"


class SpotifyStatusOut(BaseModel):
    """What the SPA needs to draw the Spotify card, in one call."""

    # Can this DEPLOYMENT do Spotify at all? False when the client credentials
    # or the token encryption key are missing/placeholders — the UI then
    # explains rather than offering a button that cannot work.
    configured: bool
    connected: bool
    # True only when the stored tokens actually still work. A connection whose
    # access was revoked on Spotify's side is connected-but-not-healthy, and the
    # user needs to reconnect.
    healthy: bool = True
    status_detail: str = ""
    spotify_user_id: str = ""
    display_name: str = ""
    product: str = ""
    scopes: str = ""
    connect_url: str = "/spotify/connect"


@api_router.get("/status", response_model=SpotifyStatusOut)
def status(
    user: User = Depends(current_user), db: Session = Depends(get_db)
) -> SpotifyStatusOut:
    settings = get_settings()
    if not settings.spotify_configured:
        return SpotifyStatusOut(
            configured=False,
            connected=False,
            status_detail="Spotify is not configured on this deployment yet.",
        )

    account = spotify.get_account(db, user.id)
    if account is None:
        return SpotifyStatusOut(configured=True, connected=False)

    healthy, detail = spotify.connection_health(db, account)
    db.commit()  # connection_health may have refreshed and rewritten tokens
    return SpotifyStatusOut(
        configured=True,
        connected=True,
        healthy=healthy,
        status_detail=detail,
        spotify_user_id=account.spotify_user_id,
        display_name=account.display_name,
        product=account.product,
        scopes=account.scopes,
    )


@api_router.post("/disconnect")
def disconnect(
    user: User = Depends(current_user), db: Session = Depends(get_db)
) -> dict:
    """Forget this user's Spotify tokens.

    This does NOT revoke the authorization on Spotify's side — Spotify offers no
    revocation endpoint. A user who wants the grant itself removed does that at
    spotify.com/account/apps; the UI says so.
    """
    removed = spotify.unlink_account(db, user.id)
    db.commit()
    return {"disconnected": removed}


@browser_router.get("/connect")
def connect(user: User = Depends(current_user)):
    """Begin the OAuth flow. A full-page redirect to Spotify's consent screen."""
    settings = get_settings()
    if not settings.spotify_configured:
        return error_response(
            "SPOTIFY_NOT_CONFIGURED",
            "Spotify is not configured on this deployment",
            503,
        )

    state = secrets.token_urlsafe(32)
    resp = RedirectResponse(spotify.authorize_url(state), status_code=302)
    resp.set_cookie(STATE_COOKIE, state, max_age=STATE_MAX_AGE, **COOKIE_KW)
    log.info("spotify: starting connect flow for user %s", user.id)
    return resp


@browser_router.get("/callback")
def callback(
    request: Request,
    user: User = Depends(current_user),
    db: Session = Depends(get_db),
):
    """Spotify redirects the browser back here.

    Which user this is comes from the APP SESSION, never from anything Spotify
    sent — so a callback cannot attach a Spotify account to somebody else.
    """
    settings = get_settings()
    if not settings.spotify_configured:
        return error_response(
            "SPOTIFY_NOT_CONFIGURED",
            "Spotify is not configured on this deployment",
            503,
        )

    def _fail(reason: str):
        log.warning("spotify: connect failed for user %s: %s", user.id, reason)
        resp = RedirectResponse(FAILED_URL, status_code=302)
        resp.delete_cookie(STATE_COOKIE, path="/")
        return resp

    # The user declined on Spotify's consent screen, or Spotify refused.
    error = request.query_params.get("error")
    if error:
        return _fail(f"spotify returned error={error}")

    code = request.query_params.get("code", "")
    state = request.query_params.get("state", "")
    expected_state = request.cookies.get(STATE_COOKIE, "")
    if not code:
        return _fail("no authorization code in callback")
    # Constant-time compare, and reject empties explicitly — an absent cookie
    # must not be allowed to match an absent state parameter.
    if not state or not expected_state or not secrets.compare_digest(
        state, expected_state
    ):
        return _fail("state mismatch")

    try:
        tokens = spotify.exchange_code(code)
        spotify.link_account(db, user.id, tokens)
    except TokenCryptoError as exc:
        db.rollback()
        return _fail(f"token encryption unavailable: {exc}")
    except spotify.SpotifyError as exc:
        db.rollback()
        return _fail(str(exc))

    db.commit()
    log.info("spotify: connected for user %s", user.id)
    resp = RedirectResponse(DONE_URL, status_code=302)
    resp.delete_cookie(STATE_COOKIE, path="/")
    return resp
