"""Приём s2s-доставки: проверка подписи вебхука Synapse.

Одна схема подписи на всю экосистему (docs/05 → «Доставка s2s»), зеркало
`app/signature.py` сервера и webhooks gnexus-auth:

    sig = "t=<unix>,v1=" + hex(hmac_sha256("<t>.<raw_body>", secret))

`verify_webhook()` — одна точка входа принимающего сервиса: проверяет
подпись (константным сравнением) и окно свежести (replay), возвращает
распарсенный конверт события. Тело — байт в байт то, что пришло в сеть,
до любого парсинга.

Использование (FastAPI-приёмник Navi):

    @router.post("/webhooks/synapse")
    async def synapse_webhook(request: Request) -> dict:
        raw = await request.body()
        envelope = verify_webhook(raw, request.headers, S2S_SECRET)
        ...  # envelope["event_id"], envelope["subject"], ...
"""

from __future__ import annotations

import hashlib
import hmac
import json
import time
from collections.abc import Mapping

from .exceptions import SynapseWebhookError

__all__ = ["SIGNATURE_HEADER", "DEFAULT_MAX_SKEW", "verify_webhook", "make_signature"]

SIGNATURE_HEADER = "x-gnexus-signature"
DEFAULT_MAX_SKEW = 300  # секунд (зеркало gnexus-auth)


def make_signature(body: bytes, secret: str, timestamp: int | None = None) -> str:
    """Подпись «сырое» тело (для тестов и отправщиков, приём — не его работа)."""
    if timestamp is None:
        timestamp = int(time.time())
    digest = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return f"t={timestamp},v1={digest}"


def verify_webhook(
    raw_body: bytes,
    headers: Mapping[str, str],
    secret: str,
    *,
    max_skew: int = DEFAULT_MAX_SKEW,
    now: int | None = None,
) -> dict[str, object]:
    """Проверить подпись и распарсить доставку. Возвращает конверт (dict).

    Тело — байт в байт как пришло (request.body(), не json-объект).
    Заголовки ищутся без учёта регистра. Ошибка → SynapseWebhookError:
    нет/битый `X-Gnexus-Signature`, replay (старше max_skew), чужой секрет,
    не-JSON тело. Все — одна ловушка, разбирать по detail.
    """
    supplied: str = ""
    for key, value in headers.items():
        if key.lower() == SIGNATURE_HEADER:
            supplied = value
            break
    parts: dict[str, str] = {}
    for chunk in supplied.split(","):
        name, _, raw_value = chunk.partition("=")
        parts[name.strip().lower()] = raw_value.strip()
    try:
        timestamp = int(parts["t"])
        digest = parts["v1"]
    except (KeyError, ValueError):
        raise SynapseWebhookError("нет или битый заголовок X-Gnexus-Signature") from None
    if not digest:
        raise SynapseWebhookError("подпись без v1-части")

    current = int(time.time()) if now is None else now
    if abs(current - timestamp) > max_skew:
        raise SynapseWebhookError(f"тело не свежее: отклонение {abs(current - timestamp)} с > {max_skew} с (replay?)")

    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, digest):
        raise SynapseWebhookError("подпись не совпала — чужой секрет или тело искажено")

    try:
        envelope = json.loads(raw_body)
    except (ValueError, UnicodeDecodeError):
        raise SynapseWebhookError("тело не JSON — приём Synapse шлёт конверт") from None
    if not isinstance(envelope, dict):
        raise SynapseWebhookError("тело — не JSON-объект конверта")
    return envelope
