# Verify a webhook delivery (Python 3.8 or later, standard library only).
import hashlib
import hmac
import re
import time

TOLERANCE_SECONDS = 300
# Deliveries already accepted (a digest of the timestamp and the body), each kept until its
# timestamp is too old to be accepted anyway. With several processes, keep them in your database.
_seen = {}


def verify_webhook(header, raw_body, secrets, now=None):
    """header:   the CC-Signature request header
    raw_body: the request body exactly as received (bytes)
    secrets:  your endpoint secret, or [new, old] while a rotation is under way
    Returns True only for a genuine delivery that has not been seen before."""
    t = None
    v1 = []
    for part in str(header or "").split(","):
        key, _, value = part.partition("=")
        key, value = key.strip(), value.strip()
        if key == "t" and re.fullmatch(r"[0-9]{1,12}", value):
            t = int(value)
        elif key == "v1" and re.fullmatch(r"[0-9a-f]{64}", value):
            v1.append(value)
    if t is None or not v1:
        return False
    now = int(time.time() if now is None else now)
    if abs(now - t) > TOLERANCE_SECONDS:
        return False
    body = raw_body if isinstance(raw_body, bytes) else str(raw_body).encode("utf-8")
    signed = str(t).encode("ascii") + b"." + body
    genuine = False
    for secret in [secrets] if isinstance(secrets, str) else secrets:
        want = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(want, got) for got in v1):
            genuine = True
    if not genuine:
        return False
    # A replay is the same timestamp and body, whichever of its signatures it still carries:
    # during a rotation a delivery has two, and resending it with only one is still a replay.
    key = hashlib.sha256(signed).hexdigest()
    for k, until in list(_seen.items()):
        if until < now:
            del _seen[k]
    if key in _seen:
        return False
    _seen[key] = t + TOLERANCE_SECONDS
    return True

# In a web framework, pass the raw body bytes, never the parsed JSON (Flask: request.get_data()).
# Answer 401 when verify_webhook() is False, else 2xx, and use the CC-Event-Id header to ignore
# an event you already handled: a retry or a replay is the same event again.
