"""Auth flow routes (mirrors /srv/apps/with/app/routers/auth.py).

Route table is deliberately minimal: the app defines ONLY /auth/login,
/auth/callback, /auth/logout — never /login, /logout, /authorize, or
/_grant at the root (those are id-auth's, and a gate snippet
reverse-proxies them; owning them here would collide — PATTERN-B.md §1b).

Flow (full code+PKCE):
    unauthenticated page hit
      -> 302 /auth/login?next=...           (middleware)
      -> 302 auth.bowden.works/app-authorize (PKCE challenge + state;
             verifier/state parked in short-lived HttpOnly cookies)
      -> 302 back to /auth/callback?code&state
      -> state checked, code exchanged server-to-server, id_token
         signature+audience+expiry VERIFIED (bw_auth.finish_login),
         Player row auto-provisioned (column-scoped), app session
         cookie minted
      -> 302 to the original destination.
"""
from __future__ import annotations

import hmac
import logging

from fastapi import APIRouter, Query, Request
from starlette.responses import JSONResponse, RedirectResponse, Response

from ..db import SessionLocal
from ..services.auth import bw_auth
from ..services.auth import session as auth_session
from ..services.auth.provision import provision_player

log = logging.getLogger("volleyboard.auth")

router = APIRouter()

FLOW_COOKIE_MAX_AGE = 600  # bw_pkce / bw_state / vb_next live 10 minutes


def _safe_next(raw: str | None) -> str:
    """Only same-origin absolute paths — never a scheme, host, or
    protocol-relative '//host' (open-redirect guard)."""
    if raw and raw.startswith("/") and not raw.startswith("//") and "\\" not in raw:
        return raw
    return "/"


def _set_flow_cookie(resp: Response, name: str, value: str, max_age: int) -> None:
    resp.set_cookie(
        name, value, max_age=max_age, httponly=True, secure=True, samesite="lax", path="/"
    )


def _auth_error(status: int, code: str, summary: str, detail: str) -> JSONResponse:
    return JSONResponse(
        status_code=status,
        content={"error_code": code, "summary": summary, "detail": detail},
    )


@router.get("/auth/login", include_in_schema=False)
def auth_login(next_url: str = Query("/", alias="next")) -> Response:
    """Start (or silently re-establish) a login: park PKCE verifier +
    state in short-lived HttpOnly cookies and bounce the browser to
    id-auth's /app-authorize. A visitor with a live BW SSO session
    round-trips without seeing a form."""
    pending: dict[str, tuple[str, int]] = {}

    def set_cookie(name: str, value: str, max_age: int) -> None:
        pending[name] = (value, max_age)

    url, _state = bw_auth.start_login(set_cookie, cookie_max_age=FLOW_COOKIE_MAX_AGE)
    resp = RedirectResponse(url=url, status_code=302)
    for name, (value, max_age) in pending.items():
        _set_flow_cookie(resp, name, value, max_age)
    destination = _safe_next(next_url)
    if destination != "/":
        _set_flow_cookie(resp, auth_session.NEXT_COOKIE, destination, FLOW_COOKIE_MAX_AGE)
    return resp


@router.get("/auth/callback", include_in_schema=False)
def auth_callback(request: Request) -> Response:
    """id-auth redirects back here. Verify state (anti-CSRF), redeem the
    code with the parked PKCE verifier, verify the id_token (signature,
    audience, expiry), provision the Player, mint the session."""
    params = request.query_params

    if params.get("error"):
        # This app never sends prompt=none, so any error here is a failed
        # interactive attempt (e.g. cancelled login) — terminal, no session.
        log.warning("auth callback returned error=%r", params.get("error"))
        return _auth_error(
            401,
            "SIGN_IN_FAILED",
            "Sign-in was not completed",
            f"id-auth returned error={params.get('error')}. Retry at /auth/login.",
        )

    code = params.get("code", "")
    state = params.get("state", "")
    expected_state = request.cookies.get(bw_auth.STATE_COOKIE, "")
    verifier = request.cookies.get(bw_auth.PKCE_COOKIE, "")

    if not code or not state or not expected_state or not hmac.compare_digest(
        state, expected_state
    ):
        return _auth_error(
            400,
            "AUTH_STATE_MISMATCH",
            "Sign-in state check failed",
            "Missing or mismatched anti-CSRF state. Restart the flow at /auth/login.",
        )
    if not verifier:
        return _auth_error(
            400,
            "AUTH_PKCE_MISSING",
            "Sign-in session expired",
            "The PKCE verifier cookie is gone (flow older than 10 minutes?). "
            "Restart at /auth/login.",
        )

    try:
        # exchange_code + verify_id_token; claims are the VERIFIED identity.
        claims = bw_auth.finish_login(code, verifier)
    except bw_auth.BWAuthError as e:
        log.warning("BW sign-in failed: %s", e)
        return _auth_error(
            401,
            "SIGN_IN_FAILED",
            "Sign-in failed",
            "The authorization code or id_token did not verify. Retry at /auth/login.",
        )

    db = SessionLocal()
    try:
        player = provision_player(db, claims)
        token = auth_session.mint_session(
            username=player.username, email=player.email or ""
        )
    finally:
        db.close()

    resp = RedirectResponse(
        url=_safe_next(request.cookies.get(auth_session.NEXT_COOKIE)), status_code=302
    )
    resp.set_cookie(
        auth_session.SESSION_COOKIE,
        token,
        max_age=auth_session.SESSION_TTL,
        httponly=True,
        secure=True,
        samesite="lax",
        path="/",
    )
    # The flow cookies are single-use.
    resp.delete_cookie(bw_auth.PKCE_COOKIE, path="/")
    resp.delete_cookie(bw_auth.STATE_COOKIE, path="/")
    resp.delete_cookie(auth_session.NEXT_COOKIE, path="/")
    return resp


@router.get("/auth/logout", include_in_schema=False)
def auth_logout() -> Response:
    """End the APP session, then bounce to id-auth's full signout so the
    shared BW SSO session is revoked too — without that the very next
    request would silently re-establish the session. Also clears any
    view-as state: impersonation never survives into the next session."""
    resp = RedirectResponse(url=bw_auth.logout_url(), status_code=302)
    resp.delete_cookie(auth_session.SESSION_COOKIE, path="/")
    resp.delete_cookie(auth_session.VIEW_AS_COOKIE, path="/")
    return resp
