"""FastAPI guard for machine routes that must arrive through a P2HS Edge Worker.

Contract: docs/COMPANION_RUNTIME_CONTRACT.md 2A.2 (EDGE-1). See ``edge_attestation`` for the
signing rules.

Modes (``P2HS_EDGE_ATTESTATION_MODE``):

* ``off``      legacy static ``X-P2HS-Gateway-Token`` check only (pre-attestation behaviour).
* ``report``   attestation is verified and failures are logged, but the legacy static token
               is still accepted. Rollout mode: lets Core deploy before the Edge signs.
* ``enforce``  attestation required. No keys, no header, a bad or replayed signature, or the
               wrong Edge class is rejected. The legacy token is NOT accepted. Fail closed.
"""
from __future__ import annotations

import hmac
import logging
import time
from collections.abc import Awaitable, Callable

from fastapi import Depends, Request

from src.config import Settings, get_settings
from src.core.edge_attestation import (
    ATTESTATION_HEADER,
    NONCE_TTL_SECONDS,
    REQUEST_ID_HEADER,
    EdgeAttestationError,
    EdgeIdentity,
    parse_keys,
    verify,
)
from src.core.exceptions import ForbiddenError
from src.core.redis import get_redis

logger = logging.getLogger(__name__)

LEGACY_TOKEN_HEADER = "X-P2HS-Gateway-Token"
MODES = frozenset({"off", "report", "enforce"})


def resolve_mode(raw: str | None) -> str:
    """Unrecognised values fail closed."""
    mode = (raw or "").strip().lower()
    return mode if mode in MODES else "enforce"


def _legacy_token_ok(request: Request, expected: str) -> bool:
    """Pre-attestation behaviour. An unset token allows everything (fail-open), which is why
    this path only exists in ``off`` and ``report`` modes."""
    if not expected:
        return True
    provided = request.headers.get(LEGACY_TOKEN_HEADER)
    return bool(provided) and hmac.compare_digest(provided, expected)


async def _check_attestation(
    request: Request, *, edge: str, settings: Settings, redis
) -> EdgeIdentity:
    keys = parse_keys(settings.edge_attestation_keys)
    body = await request.body()
    identity = verify(
        request.headers.get(ATTESTATION_HEADER),
        method=request.method,
        path=request.url.path,
        body=body,
        request_id=request.headers.get(REQUEST_ID_HEADER, ""),
        keys=keys,
        now=int(time.time()),
    )
    if identity.edge != edge:
        raise EdgeAttestationError("wrong_edge")

    # Replay protection. Shared state is required; if it is unavailable the caller decides
    # (enforce: reject; report: log and continue).
    try:
        first_use = await redis.set(
            f"edge:nonce:{identity.kid}:{identity.nonce}", "1", nx=True, ex=NONCE_TTL_SECONDS
        )
    except Exception as exc:  # noqa: BLE001 - any Redis failure is a verification failure
        raise EdgeAttestationError("nonce_store_unavailable") from exc
    if not first_use:
        raise EdgeAttestationError("replayed_nonce")
    return identity


def require_edge(
    edge: str,
    *,
    legacy_token: Callable[[Settings], str] | None,
) -> Callable[..., Awaitable[None]]:
    """Build a dependency that requires ``edge`` attestation (see module docstring).

    ``legacy_token`` returns the static token accepted in ``off``/``report`` modes. Pass
    ``None`` for routes that had no static check before: in those modes they stay open
    exactly as they were (so rollout changes nothing), while ``report`` logs who calls them
    without an attestation and ``enforce`` closes them.
    """

    async def _guard(request: Request, redis=Depends(get_redis)) -> None:
        settings = get_settings()
        mode = resolve_mode(settings.edge_attestation_mode)
        path = request.url.path
        legacy_expected = legacy_token(settings) if legacy_token is not None else ""

        if mode == "off":
            if not _legacy_token_ok(request, legacy_expected):
                raise ForbiddenError("invalid_gateway_token")
            return

        try:
            identity = await _check_attestation(
                request, edge=edge, settings=settings, redis=redis
            )
        except EdgeAttestationError as exc:
            if mode == "enforce":
                logger.warning(
                    "edge_attestation_rejected edge=%s path=%s reason=%s", edge, path, exc.code
                )
                # Generic message: never tell a caller why (or that an Edge exists).
                raise ForbiddenError("edge_attestation_required") from None
            # report mode: an absent header is expected until the Edge signs; anything else
            # (bad signature, replay, wrong edge) is suspicious and logged louder.
            level = logging.INFO if exc.code == "missing_header" else logging.WARNING
            logger.log(
                level, "edge_attestation_failed edge=%s path=%s reason=%s mode=report",
                edge, path, exc.code,
            )
            if not _legacy_token_ok(request, legacy_expected):
                raise ForbiddenError("invalid_gateway_token") from None
            return

        request.state.edge_identity = identity

    return _guard
