<?php
// No network requests. Use exact serialized bytes for both signing and transmission.
function canonicalTarget(string $target): string {
    if (!str_starts_with($target, '/') || str_starts_with($target, '//') || preg_match('/[\x00-\x20#\\\\]/', $target) || preg_match('/%(?![0-9a-fA-F]{2})/', $target)) throw new Exception('Invalid target');
    $parts = explode('?', $target);
    if (count($parts) > 2 || str_contains($parts[0], '//')) throw new Exception('Invalid target');
    $segments = array_map('rawurldecode', explode('/', $parts[0]));
    foreach ($segments as $s) if ($s === '.' || $s === '..' || preg_match('/[\x00-\x20\/\\\\]/', $s) || !preg_match('//u', $s)) throw new Exception('Invalid path');
    $path = implode('/', array_map('rawurlencode', $segments));
    $pairs = [];
    if (!empty($parts[1])) foreach (explode('&', $parts[1]) as $pair) {
        $p = explode('=', $pair, 2); $k = urldecode($p[0]); $v = urldecode($p[1] ?? '');
        if (!preg_match('//u', $k) || !preg_match('//u', $v)) throw new Exception('Invalid UTF-8');
        $pairs[] = [rawurlencode($k), rawurlencode($v)];
    }
    usort($pairs, fn($a,$b) => strcmp($a[0],$b[0]) ?: strcmp($a[1],$b[1]));
    return $path . (count($pairs) ? '?' . implode('&', array_map(fn($p) => implode('=', $p), $pairs)) : '');
}
function signRequest(string $secret, string $method, string $target, string $timestamp, string $nonce, string $raw): string {
    return hash_hmac('sha256', implode("\n", [strtoupper($method), canonicalTarget($target), $timestamp, $nonce, hash('sha256',$raw)]), $secret);
}
function signedHeaders(string $secret, string $key, string $target, string $raw): array {
    $timestamp = (string)time(); $nonce = bin2hex(random_bytes(16));
    return ['Content-Type'=>'application/json','x-api-key'=>$key,'X-PayAGL-Timestamp'=>$timestamp,'X-PayAGL-Nonce'=>$nonce,'X-PayAGL-Signature'=>signRequest($secret,'POST',$target,$timestamp,$nonce,$raw)];
}
function verifyWebhook(string $secret, string $raw, array $headers, ?int $now = null): string {
    $t = $headers['x-payagl-timestamp'] ?? ''; $id = $headers['x-payagl-delivery-id'] ?? '';
    if (($headers['x-payagl-signature-version'] ?? '') !== 'v1' || !preg_match('/^\d{10}$/D',$t) || !preg_match('/^[A-Za-z0-9_-]{16,128}$/D',$id) || abs(($now ?? time())-(int)$t)>300) throw new Exception('Invalid or expired webhook');
    if (!hash_equals(hash_hmac('sha256',$t."\n".$id."\n".$raw,$secret), $headers['x-payagl-signature'] ?? '')) throw new Exception('Invalid signature');
    return $id; // Atomically deduplicate in durable storage before business processing.
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
    foreach ([['/api/payment/create',['amount'=>500,'order_id'=>'synthetic-payin']],['/api/payout/create',['amount'=>500,'order_id'=>'synthetic-payout','beneficiary_name'=>'Synthetic','account_number'=>'12345678','ifsc'=>'HDFC0000001']]] as [$target,$body]) {
        $raw=json_encode($body,JSON_THROW_ON_ERROR|JSON_UNESCAPED_SLASHES); echo json_encode(['target'=>$target,'body'=>$raw,'headers'=>signedHeaders('synthetic-request-secret','synthetic-api-key',$target,$raw)])."\n";
    }
}
