<?php

declare(strict_types=1);

namespace ApiModel\Infrastructure\Storage;

/**
 * URLs prefirmadas compatibles con S3 / Cloudflare R2 (AWS Signature Version 4, credenciales en query string).
 * Sustituye el uso de {@see \Aws\S3\S3Client::createPresignedRequest} sin aws-sdk-php.
 */
final class S3SigV4QueryPresigner
{
    private const SERVICE = 's3';

    public function __construct(
        private readonly string $accessKeyId,
        private readonly string $secretAccessKey,
        private readonly string $region,
        private readonly string $endpoint,
    ) {
    }

    /**
     * @param array<string, string> $extraSignedHeaders nombres en minúsculas (p. ej. content-type)
     *
     * @return array{url: string, method: string}
     */
    public function presign(
        string $method,
        string $bucket,
        string $objectKey,
        int $expiresSeconds,
        array $extraSignedHeaders = [],
        ?int $nowUnix = null,
    ): array {
        $method = strtoupper($method);
        $nowUnix ??= time();
        $amzDate = gmdate('Ymd\THis\Z', $nowUnix);
        $dateStamp = gmdate('Ymd', $nowUnix);

        $parsed = parse_url($this->endpoint);
        $scheme = $parsed['scheme'] ?? 'https';
        $host = $parsed['host'] ?? '';
        if ($host === '') {
            throw new \InvalidArgumentException('R2_ENDPOINT sin host válido.');
        }
        $port = isset($parsed['port']) ? (int) $parsed['port'] : null;
        $hostHeader = $host;
        if ($port !== null && ! self::isDefaultPort($scheme, $port)) {
            $hostHeader .= ':' . $port;
        }

        $canonicalUri = self::pathStyleCanonicalUri($bucket, $objectKey);

        $signedHeaders = array_merge(
            ['host' => $hostHeader],
            $extraSignedHeaders,
        );
        ksort($signedHeaders);
        $signedHeadersList = implode(';', array_keys($signedHeaders));

        $credential = $this->accessKeyId . '/' . $dateStamp . '/' . $this->region . '/' . self::SERVICE . '/aws4_request';

        $query = [
            'X-Amz-Algorithm' => 'AWS4-HMAC-SHA256',
            'X-Amz-Credential' => $credential,
            'X-Amz-Date' => $amzDate,
            'X-Amz-Expires' => (string) $expiresSeconds,
            'X-Amz-SignedHeaders' => $signedHeadersList,
        ];
        ksort($query);
        $canonicalQueryString = self::canonicalQueryString($query);

        $canonicalHeaders = '';
        foreach ($signedHeaders as $name => $value) {
            $canonicalHeaders .= $name . ':' . trim(preg_replace('/\s+/', ' ', $value) ?? '') . "\n";
        }

        $canonicalRequest = $method . "\n"
            . $canonicalUri . "\n"
            . $canonicalQueryString . "\n"
            . $canonicalHeaders . "\n"
            . $signedHeadersList . "\n"
            . 'UNSIGNED-PAYLOAD';

        $scope = $dateStamp . '/' . $this->region . '/' . self::SERVICE . '/aws4_request';
        $stringToSign = "AWS4-HMAC-SHA256\n{$amzDate}\n{$scope}\n" . hash('sha256', $canonicalRequest);

        $signingKey = self::signingKey($this->secretAccessKey, $dateStamp, $this->region, self::SERVICE);
        $signature = hash_hmac('sha256', $stringToSign, $signingKey);

        $query['X-Amz-Signature'] = $signature;
        ksort($query);
        $finalQs = self::canonicalQueryString($query);

        $authority = $hostHeader;
        $url = $scheme . '://' . $authority . $canonicalUri . '?' . $finalQs;

        return ['url' => $url, 'method' => $method];
    }

    private static function isDefaultPort(string $scheme, int $port): bool
    {
        return ($scheme === 'https' && $port === 443) || ($scheme === 'http' && $port === 80);
    }

    private static function pathStyleCanonicalUri(string $bucket, string $objectKey): string
    {
        $bucket = trim($bucket);
        $objectKey = trim(str_replace('\\', '/', $objectKey));
        $objectKey = ltrim($objectKey, '/');

        return '/' . self::uriEncodePath($bucket) . '/' . self::uriEncodePath($objectKey);
    }

    /** Codifica cada segmento de ruta (estilo S3 / SigV4). */
    private static function uriEncodePath(string $path): string
    {
        if ($path === '') {
            return '';
        }
        $parts = explode('/', $path);

        return implode('/', array_map(static fn (string $p): string => self::encodeUriComponent($p), $parts));
    }

    private static function encodeUriComponent(string $s): string
    {
        $out = '';
        $len = strlen($s);
        for ($i = 0; $i < $len; $i++) {
            $c = $s[$i];
            if (
                ($c >= 'A' && $c <= 'Z')
                || ($c >= 'a' && $c <= 'z')
                || ($c >= '0' && $c <= '9')
                || $c === '-' || $c === '_' || $c === '.' || $c === '~'
            ) {
                $out .= $c;
            } else {
                $out .= '%' . strtoupper(bin2hex($c));
            }
        }

        return $out;
    }

    /** @param array<string, string> $params */
    private static function canonicalQueryString(array $params): string
    {
        ksort($params);
        $pairs = [];
        foreach ($params as $k => $v) {
            $pairs[] = self::encodeQuery($k) . '=' . self::encodeQuery($v);
        }

        return implode('&', $pairs);
    }

    private static function encodeQuery(string $s): string
    {
        return rawurlencode($s);
    }

    private static function signingKey(string $secret, string $dateStamp, string $region, string $service): string
    {
        $kDate = hash_hmac('sha256', $dateStamp, 'AWS4' . $secret, true);
        $kRegion = hash_hmac('sha256', $region, $kDate, true);
        $kService = hash_hmac('sha256', $service, $kRegion, true);

        return hash_hmac('sha256', 'aws4_request', $kService, true);
    }
}
