<?php

namespace App\Services\Webhooks;

class StandardWebhooks
{
    private const SECRET_PREFIX = 'whsec_';

    private const TOLERANCE = 5 * 60; // 5 minutes

    private $secret;

    public function __construct(string $secret)
    {
        if (substr($secret, 0, strlen(self::SECRET_PREFIX)) === self::SECRET_PREFIX) {
            $secret = substr($secret, strlen(self::SECRET_PREFIX));
        }

        // The secret should be base64 decoded for HMAC operations
        $this->secret = base64_decode($secret);
    }

    /**
     * Create a webhook instance from a raw secret (no base64 decoding)
     */
    public static function fromRaw(string $secret): self
    {
        $obj = new self('');
        $obj->secret = $secret;

        return $obj;
    }

    /**
     * Verify webhook signature and return decoded payload
     */
    public function verify(string $payload, array $headers): array
    {
        // Check for required headers
        if (
            ! isset($headers['webhook-id']) &&
            ! isset($headers['webhook-timestamp']) &&
            ! isset($headers['webhook-signature'])
        ) {
            throw new WebhookVerificationException('Missing required headers');
        }

        $msgId = $headers['webhook-id'] ?? null;
        $msgTimestamp = $headers['webhook-timestamp'] ?? null;
        $msgSignature = $headers['webhook-signature'] ?? null;

        if (! $msgId || ! $msgTimestamp || ! $msgSignature) {
            throw new WebhookVerificationException('Missing required headers');
        }

        // Verify timestamp is within tolerance
        $timestamp = $this->verifyTimestamp($msgTimestamp);

        // Generate expected signature
        $signature = $this->sign($msgId, $timestamp, $payload);
        $expectedSignature = explode(',', $signature, 2)[1];

        // Check all provided signatures
        $passedSignatures = explode(' ', $msgSignature);
        foreach ($passedSignatures as $versionedSignature) {
            $sigParts = explode(',', $versionedSignature, 2);
            if (count($sigParts) < 2) {
                continue;
            }

            $version = $sigParts[0];
            $passedSignature = $sigParts[1];

            if (strcmp($version, 'v1') !== 0) {
                continue;
            }

            if (hash_equals($expectedSignature, $passedSignature)) {
                return json_decode($payload, true) ?? [];
            }
        }

        throw new WebhookVerificationException('No matching signature found');
    }

    /**
     * Generate webhook signature
     */
    public function sign(string $msgId, int $timestamp, string $payload): string
    {
        $timestamp = (string) $timestamp;

        if (! $this->isPositiveInteger($timestamp)) {
            throw new WebhookSigningException('Invalid timestamp');
        }

        $toSign = "{$msgId}.{$timestamp}.{$payload}";
        $hexHash = hash_hmac('sha256', $toSign, $this->secret);
        $signature = base64_encode(pack('H*', $hexHash));

        return "v1,{$signature}";
    }

    /**
     * Verify timestamp is within acceptable tolerance
     */
    private function verifyTimestamp(string $timestampHeader): int
    {
        $now = time();

        try {
            $timestamp = intval($timestampHeader, 10);
        } catch (\Exception $e) {
            throw new WebhookVerificationException('Invalid Signature Headers');
        }

        if ($timestamp < ($now - self::TOLERANCE)) {
            throw new WebhookVerificationException('Message timestamp too old');
        }

        if ($timestamp > ($now + self::TOLERANCE)) {
            throw new WebhookVerificationException('Message timestamp too new');
        }

        return $timestamp;
    }

    /**
     * Check if value is a positive integer
     */
    private function isPositiveInteger(string $value): bool
    {
        return is_numeric($value) && ! is_float($value + 0) && (int) $value == $value && (int) $value > 0;
    }
}
