#!/usr/bin/env python3
"""Verify exact xPortfolios webhook bytes; deduplicate event_id before processing.

python webhook-verifier.py --secret-file signing.secret --body-file request.bin --headers-file headers.json
headers.json: {"X-XP-Timestamp":"...", "X-XP-Signature":"sha256=..."}
Keep the received body unchanged. Verify before parsing or making network calls.
Accept timestamps only within 300 seconds of a synchronized clock. Store event_id
durably with your processing transaction; HTTP 2xx only means HTTP acceptance.
The sender retries at 1, 5, 30 and 120 minutes after a failed attempt. Lost replies
can cause redelivery. Retries/replay keep event_id; this is at-least-once delivery.
"""
import argparse
import hashlib
import hmac
import json
from pathlib import Path
import re
import time


def verify(secret, timestamp, body, supplied, *, now=None):
    if not re.fullmatch(r'[0-9]{10,12}', timestamp or ''):
        raise ValueError('Missing or invalid timestamp')
    if abs((time.time() if now is None else now) - int(timestamp)) > 300:
        raise ValueError('Timestamp outside five-minute window')
    expected = 'sha256=' + hmac.new(secret, timestamp.encode('ascii') + b'.' + body, hashlib.sha256).hexdigest()
    if not isinstance(supplied, str) or not hmac.compare_digest(expected, supplied):
        raise ValueError('Signature does not match exact request bytes')
    event = json.loads(body)
    if event['kind'] not in ('model.published', 'model.corrected'):
        raise ValueError('Unknown event kind')
    return event


def verify_model(event, response):
    """response is the authenticated JSON at event.links.targets (Bearer header).

    The hash covers canonical ASCII JSON of response.model, sorted keys, compact
    comma/colon separators, ensure_ascii=True, no NaN and no trailing newline.
    Files have their own hashes in the SFTP manifest. No API key appears in URLs.
    """
    body = json.dumps(response['model'], sort_keys=True, separators=(',', ':'),
                      ensure_ascii=True, allow_nan=False).encode('ascii')
    return hmac.compare_digest(hashlib.sha256(body).hexdigest(), event['payload_sha256'])


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--secret-file', type=Path, required=True)
    parser.add_argument('--body-file', type=Path, required=True)
    parser.add_argument('--headers-file', type=Path, required=True)
    args = parser.parse_args()
    headers = {k.lower(): v for k, v in json.loads(args.headers_file.read_text()).items()}
    event = verify(args.secret_file.read_bytes(), headers.get('x-xp-timestamp'),
                   args.body_file.read_bytes(), headers.get('x-xp-signature'))
    print(json.dumps({'event_id': event['event_id'], 'kind': event['kind'], 'signature': 'valid',
                      'processing': 'not performed; durably deduplicate before applying'}))


if __name__ == '__main__':
    main()
