<?php

declare(strict_types=1);

namespace App\Framework\Security\RequestSigning;

use App\Framework\HttpClient\ClientRequest;
use App\Framework\HttpClient\ClientResponse;
use App\Framework\HttpClient\HttpClientMiddleware;

/**
 * HTTP Client middleware for automatically signing outgoing requests
 */
final readonly class HttpClientSigningMiddleware implements HttpClientMiddleware
{
    public function __construct(
        private SigningKey $signingKey,
        private array $headersToSign = ['(request-target)', 'host', 'date', 'digest'],
        private bool $addDigest = true,
    ) {
    }

    public function __invoke(ClientRequest $request, callable $next): ClientResponse
    {
        $signedRequest = $this->signRequest($request);

        return $next($signedRequest);
    }

    /**
     * Sign the outgoing request
     */
    private function signRequest(ClientRequest $request): ClientRequest
    {
        $headers = $request->headers;

        // Add Date header if not present
        if ($headers->getFirst('Date') === null) {
            $headers = $headers->with('Date', gmdate('D, d M Y H:i:s T'));
        }

        // Add Host header if not present
        if ($headers->getFirst('Host') === null) {
            $parsedUrl = parse_url($request->url);
            $host = $parsedUrl['host'] ?? '';
            if (isset($parsedUrl['port']) && ! in_array($parsedUrl['port'], [80, 443])) {
                $host .= ':' . $parsedUrl['port'];
            }
            $headers = $headers->with('Host', $host);
        }

        // Add Digest header for requests with body
        if ($this->addDigest && $this->hasBody($request)) {
            $body = is_string($request->body) ? $request->body : json_encode($request->body);
            $digest = $this->createDigest($body);
            $headers = $headers->with('Digest', $digest);
        }

        // Create a temporary HttpRequest for signing
        $httpRequest = new \App\Framework\Http\HttpRequest(
            method: $request->method,
            headers: $headers,
            body: is_string($request->body) ? $request->body : json_encode($request->body),
            path: parse_url($request->url, PHP_URL_PATH) ?: '/',
            queryParams: $this->parseQueryParams($request->url),
        );

        // Create signature
        $signingString = new SigningString($httpRequest);
        $stringToSign = $signingString->build($this->headersToSign);

        $signature = $this->createSignature($stringToSign);

        $requestSignature = new RequestSignature(
            signature: $signature,
            keyId: $this->signingKey->keyId,
            algorithm: $this->signingKey->algorithm,
            headers: $this->headersToSign,
            timestamp: new \DateTimeImmutable(),
        );

        // Add signature header
        $headers = $headers->with('Signature', $requestSignature->toHttpSignatureHeader());

        return $request->with(['headers' => $headers]);
    }

    /**
     * Check if request has a body
     */
    private function hasBody(ClientRequest $request): bool
    {
        if (is_string($request->body)) {
            return $request->body !== '';
        }

        return ! empty($request->body);
    }

    /**
     * Create digest for request body
     */
    private function createDigest(string $body): string
    {
        $hash = hash('sha256', $body, true);

        return 'SHA256=' . base64_encode($hash);
    }

    /**
     * Parse query parameters from URL
     */
    private function parseQueryParams(string $url): array
    {
        $parsedUrl = parse_url($url);
        if (! isset($parsedUrl['query'])) {
            return [];
        }

        parse_str($parsedUrl['query'], $params);

        return $params;
    }

    /**
     * Create signature using the signing key
     */
    private function createSignature(string $data): string
    {
        return match ($this->signingKey->algorithm) {
            SigningAlgorithm::HMAC_SHA256 => $this->createHmacSignature($data, 'sha256'),
            SigningAlgorithm::HMAC_SHA512 => $this->createHmacSignature($data, 'sha512'),
            SigningAlgorithm::RSA_SHA256 => $this->createRsaSignature($data),
            SigningAlgorithm::ED25519 => throw new \InvalidArgumentException('ED25519 not yet implemented'),
        };
    }

    /**
     * Create HMAC signature
     */
    private function createHmacSignature(string $data, string $algorithm): string
    {
        $signature = hash_hmac($algorithm, $data, $this->signingKey->keyMaterial, true);

        return base64_encode($signature);
    }

    /**
     * Create RSA signature
     */
    private function createRsaSignature(string $data): string
    {
        $privateKey = openssl_pkey_get_private($this->signingKey->keyMaterial);
        if ($privateKey === false) {
            throw new \RuntimeException('Failed to load RSA private key');
        }

        $signature = '';
        $result = openssl_sign($data, $signature, $privateKey, $this->signingKey->algorithm->getOpenSSLSignatureAlgorithm());

        if (! $result) {
            throw new \RuntimeException('Failed to create RSA signature');
        }

        return base64_encode($signature);
    }
}
