"""SSO-эндпоинты: login (redirect на gnexus-auth), callback (обмен кода), refresh, revoke.

Весь OAuth2 Authorization Code + PKCE выполняется на сервере через gnexus-gauth:
токены никогда не проходят через браузер напрямую, а роль проверяется до выдачи
токена пользователю.
"""

import logging
from urllib.parse import quote, urlencode

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from gnexus_gauth.client import GAuthClient
from pydantic import BaseModel

from app.auth.client import GAUTH_SCOPES, get_gauth_client
from app.auth.deps import ADMIN_ROLES
from app.auth.stores import revoke_logins_for_user, store_login
from app.config import Settings, get_settings

logger = logging.getLogger("synapse.auth")

router = APIRouter(prefix="/auth")


def _spa_redirect(settings: Settings, path: str, fragment: dict | None = None) -> RedirectResponse:
    """Редирект на адрес SPA (dev: SPA_PUBLIC_URL, прод: тот же origin)."""
    url = f"{settings.spa_public_url.rstrip('/')}{path}" if settings.spa_public_url else path
    if fragment:
        # Фрагмент (#) — не логируется серверами и не уходит в историю запросов.
        url += "#" + urlencode(fragment, safe="", quote_via=quote)
    return RedirectResponse(url)


@router.get("/login")
def login(
    return_to: str | None = None,
    settings=Depends(get_settings),
) -> RedirectResponse:
    """Начало SSO: state+PKCE складываются в Redis, браузер уходит на авторизацию gnexus-auth."""
    client = get_gauth_client()
    request = client.build_authorization_request(return_to=return_to, scopes=GAUTH_SCOPES)
    return RedirectResponse(request.authorization_url)


@router.get("/callback")
def callback(
    code: str | None = None,
    state: str | None = None,
    error: str | None = None,
    settings=Depends(get_settings),
) -> RedirectResponse:
    """Код от gnexus-auth -> TokenSet. Пользователю без роли admin токен не выдаётся."""
    if error or not code or not state:
        return _spa_redirect(settings, "/login", {"error": error or "missing_code_or_state"})

    client = get_gauth_client()
    try:
        tokens = client.exchange_authorization_code(code, state)
    except Exception:
        return _spa_redirect(settings, "/login", {"error": "exchange_failed"})

    try:
        user = client.fetch_user(tokens.access_token)
    except Exception:
        return _spa_redirect(settings, "/login", {"error": "userinfo_failed"})

    if (user.system_role or "") not in ADMIN_ROLES:
        # Отзываем access-токен сразу: он валиден ~30 минут, не оставляем его пользователю.
        try:
            client.revoke_token(tokens.access_token)
        except Exception:
            pass
        return _spa_redirect(settings, "/denied", {})

    # Учёт входа: при выходе пользователя из gnexus-auth вебхук найдёт
    # этот токен и отзовёт его (см. webhooks()).
    store_login(user.user_id, state, tokens.access_token, tokens.refresh_token)

    fragment = {
        "access_token": tokens.access_token,
        "expires_in": tokens.expires_in,
        "token_type": tokens.token_type,
    }
    if tokens.refresh_token:
        fragment["refresh_token"] = tokens.refresh_token
    return _spa_redirect(settings, "/oauth/callback", fragment)


class RefreshRequest(BaseModel):
    refresh_token: str


@router.post("/refresh")
def refresh(body: RefreshRequest) -> dict:
    """Обновление access-токена (SPA делает это тихо до истечения текущего)."""
    try:
        tokens = get_gauth_client().refresh_token(body.refresh_token)
    except Exception:
        raise HTTPException(status_code=401, detail="Refresh-токен невалиден") from None
    payload: dict = {
        "access_token": tokens.access_token,
        "expires_in": tokens.expires_in,
    }
    if tokens.refresh_token:
        payload["refresh_token"] = tokens.refresh_token
    return payload


@router.post("/logout")
def full_logout(request: Request) -> dict:
    """Выход из Synapse: revoke всех известных входов пользователя (access+refresh).

    SSO-сессия gnexus-auth (web-cookie) пока не завершается — RP-initiated
    logout требует поддержки на стороне gnexus-auth; зафиксировано в #32.
    После revoke любой API-запрос и heartbeat получат 401.
    """
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Требуется Bearer-токен")
    client = get_gauth_client()
    token = auth.removeprefix("Bearer ")
    user_id: str | None = None
    try:
        user = client.fetch_user(token)  # токен ещё жив — узнаём, чей выход
        user_id = str(user.user_id)
    except Exception:
        pass  # токен уже мёртв — просто чистим локально по index'у
    revoked = revoke_logins_for_user(user_id, client) if user_id else 0
    # текущий (не учтён в store_login, если вход был до этой функции) — добиваем явно
    try:
        client.revoke_token(token, "access_token")
    except Exception:
        pass
    return {"revoked": True, "logins_killed": revoked}


@router.post("/webhooks")
async def webhooks(request: Request) -> dict:
    """Приём вебхуков gnexus-auth (user.*, role.*, client.*, session.*, auth.*, webhook.test).

    Подпись HMAC (`t=...,v1=...`) проверяется через gnexus-gauth с секретом
    из env.

    Ключевое правило single sign-out: gnexus-auth при выходе НЕ отзывает
    OAuth-токены, поэтому выход из auth должен долететь до нас через
    auth.logout / auth.global_logout / session.revoked — по user_id мы
    отзываем все выданные этому пользователю access+refresh.
    """
    secret = get_settings().gauth_webhook_secret
    if not secret:
        raise HTTPException(status_code=503, detail="GAUTH_WEBHOOK_SECRET не настроен")

    raw = (await request.body()).decode("utf-8", errors="replace")
    try:
        event = get_gauth_client().verify_and_parse_webhook(raw, dict(request.headers), secret)
    except Exception:
        raise HTTPException(status_code=400, detail="Некорректная подпись вебхука") from None

    if event.event_type in ("auth.logout", "auth.global_logout", "session.revoked"):
        payload = event.raw_payload or {}
        user = payload.get("user") or {}
        target_user = user.get("id") or event.target_identifiers.get("user_id")
        killed = revoke_logins_for_user(target_user, get_gauth_client()) if target_user else 0
        if target_user:
            logger.info(
                "synapse logout по вебхуку: user=%s type=%s revoked_logins=%d",
                target_user, event.event_type, killed,
            )
        else:
            logger.warning("logout-событие %s без user.id — нечем отзвать токены", event.event_type)
    else:
        logger.info("gauth webhook: %s id=%s target=%s", event.event_type, event.event_id, event.target_identifiers)

    return {"received": True, "event_type": event.event_type}