<?php

namespace App\Http\Middleware;

use App\Models\ApiKey;
use App\Support\ApiResponse;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

final class AuthenticateApiKey
{
    public function handle(Request $request, Closure $next): Response
    {
        $publicKey = (string) $request->header('x-api-key', '');
        $timestamp = (string) $request->header('x-api-timestamp', '');
        $nonce = (string) $request->header('x-api-nonce', '');
        $signatureHeader = (string) $request->header('x-api-signature', '');

        if ($publicKey === '' || $timestamp === '' || $nonce === '' || $signatureHeader === '') {
            return ApiResponse::error('INVALID_SIGNATURE', 'Required API authentication headers are missing.', 401);
        }

        if (! ctype_digit($timestamp)) {
            return ApiResponse::error('REPLAY_DETECTED', 'Request timestamp is outside the allowed five-minute window.', 401);
        }

        $timestampForSkew = strlen($timestamp) >= 13
            ? intdiv((int) $timestamp, 1000)
            : (int) $timestamp;

        if (abs(now()->timestamp - $timestampForSkew) > config('gateway.clock_skew_seconds', 300)) {
            return ApiResponse::error('REPLAY_DETECTED', 'Request timestamp is outside the allowed five-minute window.', 401);
        }

        if (! preg_match('/^v1=([a-f0-9]{64})$/i', $signatureHeader, $matches)) {
            return ApiResponse::error('INVALID_SIGNATURE', 'x-api-signature must use the v1=<hex-hmac> format.', 401);
        }

        $apiKey = ApiKey::query()
            ->with('merchant')
            ->where('public_key_hash', hash('sha256', $publicKey))
            ->where('status', 'active')
            ->first();

        if (! $apiKey) {
            return ApiResponse::error('INVALID_API_KEY', 'Invalid or revoked API key.', 401);
        }

        $path = $this->canonicalPath($request);
        $rawBody = $request->getContent(); // Exact bytes received; never decode and re-encode JSON.
        $canonical = implode("\n", [
            strtoupper($request->method()),
            $path,
            $timestamp,
            $nonce,
            hash('sha256', $rawBody),
        ]);

        // ApiKey encrypted cast decrypts the HMAC secret only in memory.
        $expected = hash_hmac('sha256', $canonical, $apiKey->secret_ciphertext);
        $provided = strtolower($matches[1]);

        if (! hash_equals($expected, $provided)) {
            return ApiResponse::error('INVALID_SIGNATURE', 'Request signature verification failed.', 401);
        }

        // Store both nonce and signature fingerprints after successful verification.
        $ttl = config('gateway.nonce_ttl_seconds', 300);
        $nonceFingerprint = hash('sha256', $nonce);
        $signatureFingerprint = hash('sha256', $provided);
        $nonceKey = "gateway:replay:nonce:{$apiKey->id}:{$nonceFingerprint}";
        $signatureKey = "gateway:replay:signature:{$apiKey->id}:{$signatureFingerprint}";

        if (! Cache::add($nonceKey, true, $ttl)) {
            return ApiResponse::error('REPLAY_DETECTED', 'This request nonce has already been used.', 401);
        }

        if (! Cache::add($signatureKey, true, $ttl)) {
            Cache::forget($nonceKey);
            return ApiResponse::error('REPLAY_DETECTED', 'This request signature has already been used.', 401);
        }

        $apiKey->forceFill(['last_used_at' => now()])->save();
        $request->attributes->set('apiKey', $apiKey);
        $request->attributes->set('api_key_model', $apiKey);
        $request->attributes->set('merchant', $apiKey->merchant);

        return $next($request);
    }

    private function canonicalPath(Request $request): string
    {
        $query = $request->query();
        $query = $this->sortRecursively($query);
        $path = $request->getPathInfo();

        return $query === []
            ? $path
            : $path.'?'.http_build_query($query, '', '&', PHP_QUERY_RFC3986);
    }

    private function sortRecursively(array $value): array
    {
        ksort($value, SORT_STRING);
        foreach ($value as $key => $item) {
            if (is_array($item)) {
                $value[$key] = $this->sortRecursively($item);
            }
        }
        return $value;
    }
}
