"""Helpers para verificação e parsing de webhooks da AbacatePay.

A AbacatePay assina os payloads de webhook com HMAC-SHA256 usando o `secret`
informado no momento da criação. Para evitar ataques de timing, sempre
comparamos assinaturas com :func:`hmac.compare_digest`.

Uso típico (Flask):

    from flask import request, abort
    from abacapython import webhooks

    @app.post("/abacatepay/webhook")
    def receive():
        signature = request.headers.get("X-AbacatePay-Signature", "")
        try:
            event = webhooks.parse_event(
                payload=request.data,
                signature=signature,
                secret=os.environ["ABACATEPAY_WEBHOOK_SECRET"],
            )
        except webhooks.WebhookSignatureError:
            abort(401)
        ...
"""

from __future__ import annotations

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

from .exceptions import WebhookSignatureError
from .models import WebhookEvent

PayloadInput = bytes | bytearray | memoryview | str

# Cabeçalhos historicamente usados pela AbacatePay para assinatura.
# Mantemos uma lista para sermos resilientes a variações de nome.
SIGNATURE_HEADERS: tuple[str, ...] = (
    "x-abacatepay-signature",
    "x-abacate-signature",
    "abacatepay-signature",
    "x-signature",
)


def _to_bytes(payload: PayloadInput) -> bytes:
    if isinstance(payload, (bytes, bytearray, memoryview)):
        return bytes(payload)
    if isinstance(payload, str):
        return payload.encode("utf-8")
    raise TypeError(f"payload precisa ser bytes ou str, recebido {type(payload).__name__}")


def compute_signature(payload: PayloadInput, secret: str) -> str:
    """Calcula a assinatura HMAC-SHA256 hex do payload.

    Args:
        payload: Corpo cru da requisição (bytes ou str).
        secret: Segredo configurado no webhook.

    Returns:
        Assinatura em hexadecimal.
    """
    digest = hmac.new(secret.encode("utf-8"), _to_bytes(payload), hashlib.sha256)
    return digest.hexdigest()


def _normalize_signature(signature: str) -> str:
    """Remove prefixos comuns como ``sha256=...`` e espaços."""
    signature = signature.strip()
    if "=" in signature and not signature.startswith("="):
        prefix, _, rest = signature.partition("=")
        if prefix.lower() in {"sha256", "hmac"}:
            return rest.strip()
    return signature


def verify_signature(payload: PayloadInput, signature: str, secret: str) -> bool:
    """Verifica se a assinatura recebida bate com a esperada.

    Args:
        payload: Corpo cru da requisição.
        signature: Valor do header de assinatura.
        secret: Segredo configurado no webhook.

    Returns:
        ``True`` se a assinatura é válida.

    Raises:
        WebhookSignatureError: Quando a assinatura não confere.
    """
    expected = compute_signature(payload, secret)
    received = _normalize_signature(signature or "")
    if not received or not hmac.compare_digest(expected, received):
        raise WebhookSignatureError("Assinatura HMAC do webhook é inválida.")
    return True


def extract_signature(headers: Mapping[str, str]) -> str:
    """Extrai a assinatura de uma estrutura de headers HTTP genérica.

    Aceita dicts case-sensitive ou case-insensitive (como `Werkzeug.Headers`).
    """
    lookup = {k.lower(): v for k, v in headers.items()}
    for header in SIGNATURE_HEADERS:
        if header in lookup:
            return lookup[header]
    return ""


def parse_event(payload: PayloadInput, signature: str, secret: str) -> WebhookEvent:
    """Valida a assinatura e retorna o payload como :class:`WebhookEvent`.

    Args:
        payload: Corpo cru da requisição.
        signature: Valor do header de assinatura.
        secret: Segredo configurado no webhook.

    Returns:
        Evento já parseado.

    Raises:
        WebhookSignatureError: Quando a assinatura é inválida.
        ValueError: Quando o payload não é um JSON válido.
    """
    verify_signature(payload, signature, secret)
    raw = _to_bytes(payload).decode("utf-8")
    if not raw:
        raise ValueError("Payload do webhook está vazio.")
    body = json.loads(raw)
    if not isinstance(body, dict):
        raise ValueError("Payload do webhook não é um objeto JSON.")
    if "data" in body and isinstance(body["data"], dict) and "event" not in body:
        # Algumas APIs envolvem o evento em `data`. Achatamos para usar como WebhookEvent.
        envelope_data = body["data"]
        if "event" in envelope_data:
            body = envelope_data
    return WebhookEvent.model_validate(body)


__all__ = [
    "WebhookSignatureError",
    "compute_signature",
    "extract_signature",
    "parse_event",
    "verify_signature",
]
