"""Clerk concrete :class:`WebhookVerifier` (per ADR-004 + ADR-016).

Single responsibility: **Svix-Signature webhook verification** for
Clerk's organisation-membership / user / session events. Same HMAC
scheme as :class:`MockWebhookVerifier`; the HMAC dance now lives in the
shared primitive :func:`mgf.common.webhooks.verify_svix_hmac` (PAPER-26),
so this adapter keeps only the Clerk semantics (``whsec_`` secret, typed
401s) and delegates the crypto.

The verifier is **stateless** and carries no JWT / session concern —
that lives in :class:`ClerkSessionVerifier`. The two were split out of
the former combined ``ClerkAuthProvider`` so a future webhook-transport
swap (or a session-provider swap) touches only the matching wiring.
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from mgf.common.webhooks import WebhookHmacError, verify_svix_hmac

from mgf.auth._errors import InvalidWebhookSignatureError
from mgf.auth._events import WebhookEvent

if TYPE_CHECKING:
    from collections.abc import Mapping

__all__ = ["ClerkWebhookVerifier"]

LOG = logging.getLogger(__name__)

# Svix's allowed clock skew for replay protection (matches the
# upstream library's default — Clerk uses Svix for webhook delivery).
_SVIX_REPLAY_WINDOW_SECONDS = 5 * 60


class ClerkWebhookVerifier:
    """Production-shape :class:`WebhookVerifier` backed by Clerk / Svix.

    Conforms structurally to the
    :class:`~plasmamapper.providers.auth.WebhookVerifier` Protocol; the
    Protocol is ``runtime_checkable`` so ``isinstance(verifier,
    WebhookVerifier)`` returns True.

    Constructor is keyword-only; concrete kwargs:

      * ``webhook_secret`` — Svix shared secret, ``whsec_...`` form,
        bytes or str. Source: Clerk webhook endpoint configuration.
    """

    def __init__(self, *, webhook_secret: bytes | str) -> None:
        # Store the RAW secret: verify_svix_hmac decodes the ``whsec_`` form
        # internally (exactly once). Pre-decoding here would double-decode and
        # break every signature.
        self._webhook_secret = webhook_secret

    # ---- WebhookVerifier Protocol implementation -------------------

    async def verify_webhook(self, headers: Mapping[str, str], body: bytes) -> WebhookEvent:
        h = {k.lower(): v for k, v in headers.items()}
        self._verify_svix_signature(h, body)

        try:
            payload = json.loads(body)
        except json.JSONDecodeError as exc:
            raise InvalidWebhookSignatureError("body is not valid JSON") from exc
        if not isinstance(payload, dict):
            raise InvalidWebhookSignatureError("body is not a JSON object")

        event_type = payload.get("type")
        if not isinstance(event_type, str):
            raise InvalidWebhookSignatureError("missing 'type' string")

        msg_id = h.get("svix-id")
        if not msg_id:
            raise InvalidWebhookSignatureError("svix-id header missing")

        event_payload: dict[str, object] = dict(payload)

        return WebhookEvent(
            event_type=event_type,
            event_id=msg_id,
            payload=event_payload,
            received_at=datetime.now(UTC),
        )

    # ---- internals -------------------------------------------------

    def _verify_svix_signature(self, headers: Mapping[str, str], body: bytes) -> None:
        # Delegate the full Svix HMAC dance (header extraction, replay window,
        # UTF-8 guard, constant-time compare, ``whsec_`` decode) to the shared
        # primitive; translate its framework-neutral failure into our typed 401.
        try:
            verify_svix_hmac(
                headers,
                body,
                self._webhook_secret,
                replay_window_seconds=_SVIX_REPLAY_WINDOW_SECONDS,
            )
        except WebhookHmacError as exc:
            raise InvalidWebhookSignatureError(exc.reason) from exc
