"""Webhook signature verification for incoming gaiia webhooks.

Each delivery includes the header
``X-Gaiia-Webhook-Signature: t=<unix-ts>,v1=<hex-sha256-hmac>``. The signature
is HMAC-SHA256 over ``"${t}.${rawBody}"`` keyed by the endpoint secret. Compare
in constant time and reject if ``t`` is outside your tolerance window.
"""

from __future__ import annotations

import hashlib
import hmac
import time
from typing import Iterable

from gaiia.errors import GaiiaError

SIGNATURE_HEADER = "X-Gaiia-Webhook-Signature"
DEFAULT_TOLERANCE_SECONDS = 5 * 60


class WebhookSignatureError(GaiiaError):
    """Raised when a webhook signature fails verification."""


def _parse_header(header_value: str) -> tuple[str | None, list[str]]:
    timestamp: str | None = None
    v1_signatures: list[str] = []
    for chunk in header_value.split(","):
        if "=" not in chunk:
            continue
        prefix, _, value = chunk.partition("=")
        prefix = prefix.strip()
        value = value.strip()
        if prefix == "t":
            timestamp = value
        elif prefix == "v1":
            v1_signatures.append(value)
    return timestamp, v1_signatures


def _compute_signature(secret: str | bytes, timestamp: str, raw_body: str | bytes) -> str:
    if isinstance(secret, str):
        secret = secret.encode("utf-8")
    if isinstance(raw_body, bytes):
        body_text = raw_body.decode("utf-8")
    else:
        body_text = raw_body
    payload = f"{timestamp}.{body_text}".encode("utf-8")
    return hmac.new(secret, payload, hashlib.sha256).hexdigest()


def verify_webhook_signature(
    *,
    header_value: str,
    raw_body: str | bytes,
    secret: str | bytes,
    tolerance_seconds: int = DEFAULT_TOLERANCE_SECONDS,
    now: float | None = None,
) -> None:
    """Verify a webhook delivery. Raises :class:`WebhookSignatureError` on failure.

    ``raw_body`` must be the exact bytes/string the server signed — do not
    re-serialize a parsed JSON body, since whitespace differences will break
    the HMAC.
    """
    if not header_value:
        raise WebhookSignatureError("missing signature header")
    timestamp, candidates = _parse_header(header_value)
    if timestamp is None:
        raise WebhookSignatureError("signature header missing timestamp (t=...)")
    if not candidates:
        raise WebhookSignatureError("signature header missing v1 signatures")

    try:
        ts_int = int(timestamp)
    except ValueError as e:
        raise WebhookSignatureError(f"signature timestamp not an integer: {timestamp!r}") from e

    current = time.time() if now is None else now
    if abs(current - ts_int) > tolerance_seconds:
        raise WebhookSignatureError(
            f"signature timestamp outside tolerance "
            f"({tolerance_seconds}s): {abs(current - ts_int):.0f}s drift"
        )

    expected = _compute_signature(secret, timestamp, raw_body)
    if not _any_match(expected, candidates):
        raise WebhookSignatureError("no signature matched expected HMAC")


def _any_match(expected: str, candidates: Iterable[str]) -> bool:
    return any(hmac.compare_digest(expected, c) for c in candidates)
