<?php

declare(strict_types=1);

namespace GNexus\GAuth\Webhook;

use GNexus\GAuth\Config\GAuthConfig;
use GNexus\GAuth\Contract\ClockInterface;
use GNexus\GAuth\Contract\WebhookVerifierInterface;
use GNexus\GAuth\DTO\VerifiedWebhook;
use GNexus\GAuth\Exception\WebhookVerificationException;
use GNexus\GAuth\Support\SystemClock;

final readonly class HmacWebhookVerifier implements WebhookVerifierInterface
{
    private ClockInterface $clock;

    public function __construct(
        private GAuthConfig $config,
        ?ClockInterface $clock = null,
    ) {
        $this->clock = $clock ?? new SystemClock();
    }

    public function verify(string $rawBody, array $headers, string $secret): VerifiedWebhook
    {
        $normalized = $this->normalizeHeaders($headers);

        foreach (['x-gnexus-event-id', 'x-gnexus-event-type', 'x-gnexus-event-timestamp', 'x-gnexus-signature'] as $requiredHeader) {
            if (! isset($normalized[$requiredHeader]) || $normalized[$requiredHeader] === '') {
                throw new WebhookVerificationException(sprintf('Missing webhook header: %s.', $requiredHeader));
            }
        }

        $signature = $this->parseSignatureHeader($normalized['x-gnexus-signature']);
        $timestamp = $signature['timestamp'];
        $expectedSignature = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

        if (! hash_equals($expectedSignature, $signature['hash'])) {
            throw new WebhookVerificationException('Invalid webhook signature.');
        }

        $tolerance = $this->config->webhookToleranceSeconds();

        if ($tolerance > 0 && abs($this->clock->now()->getTimestamp() - $timestamp) > $tolerance) {
            throw new WebhookVerificationException('Webhook timestamp is outside the allowed tolerance window.');
        }

        return new VerifiedWebhook(
            rawBody: $rawBody,
            normalizedHeaders: $normalized,
            signatureId: $normalized['x-gnexus-event-id'],
            verifiedAt: $this->clock->now(),
        );
    }

    /**
     * @param array<string, mixed> $headers
     * @return array<string, string>
     */
    private function normalizeHeaders(array $headers): array
    {
        $normalized = [];

        foreach ($headers as $name => $value) {
            $normalizedName = strtolower((string) $name);
            $normalizedName = str_replace('_', '-', $normalizedName);

            if (is_array($value)) {
                $value = $value[0] ?? '';
            }

            $normalized[$normalizedName] = trim((string) $value);
        }

        return $normalized;
    }

    /**
     * @return array{timestamp:int, hash:string}
     */
    private function parseSignatureHeader(string $header): array
    {
        $parts = [];

        foreach (explode(',', $header) as $chunk) {
            [$key, $value] = array_pad(explode('=', trim($chunk), 2), 2, null);

            if ($key !== null && $value !== null) {
                $parts[$key] = $value;
            }
        }

        if (! isset($parts['t'], $parts['v1'])) {
            throw new WebhookVerificationException('Malformed webhook signature header.');
        }

        if (! ctype_digit($parts['t'])) {
            throw new WebhookVerificationException('Webhook timestamp must be numeric.');
        }

        return [
            'timestamp' => (int) $parts['t'],
            'hash' => strtolower($parts['v1']),
        ];
    }
}

