"""The BW Auth (Pattern B) sign-in routes.

Scout owns /auth/* and /logout — which is why this host must never be gated with
`import id-auth`. The gate would claim these paths and sign-in would 404.

The flow is the SPA variant: the client asks /api/me whether to probe, and drives
a full-page navigation to /auth/probe when the answer is yes. See
/srv/system/id-auth/app-auth/PATTERN-B.md.
"""

import logging
import time

from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session

from app import bw_auth
from app.config import get_settings
from app.db import get_db
from app.models.schemas import ForgotRequest
from app.services import bw, session as app_session
from app.services import levels as levels_service

log = logging.getLogger(__name__)
router = APIRouter(tags=["auth"], include_in_schema=False)

# The PKCE verifier and the CSRF nonce cross a redirect round-trip and must not
# be readable by JavaScript.
_COOKIE_KW = {"httponly": True, "secure": True, "samesite": "lax"}


def _collect_cookies() -> tuple[dict, callable]:
    """bw_auth hands cookies back through a callback; gather them so they can be
    applied to the redirect response once it exists."""
    pending: dict[str, tuple[str, int]] = {}

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

    return pending, set_cookie


def _apply(response: RedirectResponse, pending: dict) -> RedirectResponse:
    for name, (value, max_age) in pending.items():
        response.set_cookie(name, value, max_age=max_age, **_COOKIE_KW)
    return response


def _require_bw_configured() -> None:
    if not get_settings().has_bw_client:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail={
                "error_code": "BW_AUTH_NOT_CONFIGURED",
                "summary": "Sign-in is not configured yet.",
                "details": "The owner needs to run srv-gw app-client-register for this host.",
            },
        )


@router.get("/auth/probe")
def auth_probe() -> RedirectResponse:
    """Silent (prompt=none) sign-in attempt for a visitor who may already be
    signed in to BW elsewhere. Sets the loop-guard cookie first, so an anonymous
    visitor is probed at most once per window rather than on every page load."""
    _require_bw_configured()
    pending, set_cookie = _collect_cookies()
    url, _state = bw_auth.start_probe(set_cookie)
    return _apply(RedirectResponse(url, status_code=302), pending)


@router.get("/auth/login")
def auth_login() -> RedirectResponse:
    """An explicit "Sign in" click: interactive login. A visitor without a BW
    session lands on the BW-hosted login page, which is correct — Scout must not
    host a credential form of its own."""
    _require_bw_configured()
    pending, set_cookie = _collect_cookies()
    url, _state = bw_auth.start_login(set_cookie, silent=False)
    response = _apply(RedirectResponse(url, status_code=302), pending)
    # Clicking "Sign in" is an explicit opt-in; drop the post-logout opt-out and
    # the probe guard so the click is never swallowed by an earlier sign-out.
    response.delete_cookie(bw.OPTOUT_COOKIE)
    response.delete_cookie(bw.PROBED_COOKIE)
    return response


@router.get("/auth/callback")
def auth_callback(request: Request, db: Session = Depends(get_db)) -> RedirectResponse:
    """Where id-auth sends the browser back.

    Two outcomes: the silent probe found no BW session (anonymous — bounce home
    and let the SPA draw its own sign-in screen, with the guard still set so it
    does not re-probe), or a real authorization code to redeem.
    """
    params = dict(request.query_params)
    if bw_auth.is_login_required(params):
        return RedirectResponse("/", status_code=302)

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

    # Anti-CSRF: the state handed back must match the one set before redirecting.
    if not code or not state or not expected_state or state != expected_state:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": "INVALID_STATE",
                "summary": "Sign-in could not be completed.",
                "details": "Start again from the sign-in screen.",
            },
        )
    if not verifier:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": "MISSING_PKCE_VERIFIER",
                "summary": "Sign-in took too long and expired.",
                "details": "Start again from the sign-in screen.",
            },
        )

    try:
        # exchange_code + verify_id_token. The token is verified, read, discarded.
        claims = bw_auth.finish_login(code, verifier)
    except bw.BWAuthError as exc:
        log.warning("BW sign-in failed: %s", exc)
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail={"error_code": "SIGN_IN_FAILED", "summary": "Sign-in failed."},
        ) from exc

    app_session.establish(request, db, claims)

    response = RedirectResponse("/", status_code=302)
    # PKCE and state are single-use.
    response.delete_cookie(bw.PKCE_COOKIE)
    response.delete_cookie(bw.STATE_COOKIE)
    bw_auth.clear_probe_cookies(response.delete_cookie)
    return response


@router.api_route("/logout", methods=["GET", "POST"])
def logout(request: Request) -> RedirectResponse:
    """Clear the Scout session, mark the visitor as opted out so the silent probe
    does not sign them straight back in, then revoke the BW session too."""
    app_session.clear(request)
    target = bw.logout_url() if get_settings().has_bw_client else "/"
    response = RedirectResponse(target, status_code=302)

    def set_cookie(name: str, value: str, max_age: int) -> None:
        response.set_cookie(name, value, max_age=max_age, **_COOKIE_KW)

    bw_auth.mark_opted_out(set_cookie)
    response.delete_cookie(bw.PROBED_COOKIE)
    return response


# Anonymous forgot-password (D32). Deliberately boring from the outside: the
# response is identical whatever happens, so it cannot confirm that a username
# exists. The server-side scope (only users Scout has reported) and the
# generic response live at the gateway; this throttle just keeps a script from
# hammering the endpoint through us.
_FORGOT_WINDOW_SECONDS = 60
_forgot_seen: dict[str, float] = {}


@router.post("/auth/forgot")
def forgot_password(payload: ForgotRequest, request: Request) -> dict:
    username = payload.username.strip().lower()
    client_ip = (request.headers.get("x-forwarded-for", "").split(",")[0].strip()
                 or (request.client.host if request.client else "?"))
    now = time.monotonic()
    for key in [f"u:{username}", f"ip:{client_ip}"]:
        if now - _forgot_seen.get(key, 0.0) < _FORGOT_WINDOW_SECONDS:
            return _FORGOT_RESPONSE  # throttled, indistinguishable on purpose
        _forgot_seen[key] = now
    if len(_forgot_seen) > 5000:
        _forgot_seen.clear()  # crude but sufficient: the window is short

    if get_settings().has_bw_client:
        try:
            levels_service.send_reset(username)
        except Exception as exc:  # noqa: BLE001 - never differentiate outcomes
            log.info("forgot-password: reset not sent for %r (%s)", username, exc)
    return _FORGOT_RESPONSE


_FORGOT_RESPONSE = {
    "ok": True,
    "summary": "If that account exists and can receive resets, an email is on its way.",
}
