"""Payment webhook verification example.

Requires Python 3.10+ and jsonschema 4.18+. Keep webhook-contract.json beside
this file. This module verifies requests; the caller owns durable receipt,
deduplication, business processing, and HTTP responses.
"""
import base64
from datetime import datetime
import hashlib
import hmac
import json
from pathlib import Path
import re
import time

from jsonschema import Draft202012Validator, FormatChecker


class WebhookError(ValueError):
    """The request cannot be accepted as an authenticated, valid event."""


_schema = json.loads(Path(__file__).with_name('webhook-contract.json').read_text())
_validator = Draft202012Validator(_schema, format_checker=FormatChecker())


def _unique_object(pairs):
    result = {}
    for name, value in pairs:
        if name in result:
            raise WebhookError('Duplicate JSON key')
        result[name] = value
    return result


def _reject_constant(value):
    raise WebhookError('Non-JSON numeric constant')


def verify_webhook(raw_body, headers, keyring, *, now=None, window_seconds=300):
    """Return a verified event. Supply now only for deterministic local tests.

    keyring maps trusted key IDs to secret, app_id, environment, endpoint_id,
    revoked, and optional not_after (Unix seconds). Select it for the receiving
    endpoint before calling. Configure HTTP body/header size limits upstream.
    """
    if not isinstance(raw_body, bytes):
        raise WebhookError('Expected original request bytes')
    if type(window_seconds) is not int or window_seconds <= 0:
        raise WebhookError('Invalid timestamp tolerance')
    current = time.time() if now is None else now
    normalized = {}
    for name, value in headers.items():
        lower = name.lower()
        if lower in normalized:
            raise WebhookError('Duplicate header')
        normalized[lower] = value
    def header(name):
        value = normalized.get(name)
        if not isinstance(value, str) or not value:
            raise WebhookError('Missing or invalid signature header')
        return value
    key_id = header('x-key-id')
    timestamp = header('x-timestamp')
    signature = header('x-signature')
    event_id = header('x-event-id')
    if not re.fullmatch(r'[1-9][0-9]{0,11}', timestamp):
        raise WebhookError('Invalid timestamp')
    if abs(current - int(timestamp)) > window_seconds:
        raise WebhookError('Timestamp outside allowed window')
    key = keyring.get(key_id)
    if not key or key.get('revoked', True):
        raise WebhookError('Unknown or revoked key')
    if key.get('not_after') is not None and current >= key['not_after']:
        raise WebhookError('Signing key validity ended')
    if not re.fullmatch(r'[A-Za-z0-9_-]{43}', signature):
        raise WebhookError('Invalid signature encoding')
    secret = key.get('secret')
    if not isinstance(secret, str) or not secret:
        raise WebhookError('Invalid local key configuration')
    digest = hmac.new(secret.encode('utf-8'), timestamp.encode('ascii') + b'.' + raw_body, hashlib.sha256).digest()
    expected = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
    if not hmac.compare_digest(expected, signature):
        raise WebhookError('Signature mismatch')
    try:
        event = json.loads(raw_body.decode('utf-8'), object_pairs_hook=_unique_object,
                           parse_constant=_reject_constant)
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise WebhookError('Invalid UTF-8 JSON') from exc
    if not isinstance(event, dict):
        raise WebhookError('Expected a JSON object')
    if event.get('event_id') != event_id:
        raise WebhookError('Event ID mismatch')
    if event.get('app_id') != key.get('app_id') or event.get('environment') != key.get('environment'):
        raise WebhookError('Application or environment mismatch')
    if not _validator.is_valid(event):
        raise WebhookError('Unsupported or invalid event schema')
    data = event['data']
    if event['type'] == 'endpoint.verification':
        if data['endpoint_id'] != key.get('endpoint_id'):
            raise WebhookError('Endpoint mismatch')
        expiry = datetime.fromisoformat(data['expires_at'].replace('Z', '+00:00')).timestamp()
        if current >= expiry:
            raise WebhookError('Verification challenge expired')
    else:
        if 'app_id' in data and data['app_id'] != event['app_id']:
            raise WebhookError('Resource application mismatch')
        if 'environment' in data and data['environment'] != event['environment']:
            raise WebhookError('Resource environment mismatch')
        if data.get('version') != event['resource_version']:
            raise WebhookError('Resource version mismatch')
    return event
