<?php

namespace App\Service;

/**
 * Génère une URL pré-signée Amazon S3 (Signature Version 4, requête GET)
 * entièrement en local (HMAC-SHA256) : aucun SDK, aucune sortie réseau — ce qui
 * la rend utilisable malgré le blocage réseau d'OVH. Un <img src> vers cette URL
 * n'est pas soumis au contrôle CORS (chargement de ressource « no-cors »).
 */
final class S3Presigner
{
    public function __construct(private readonly S3Settings $s3)
    {
    }

    /**
     * @throws \RuntimeException si la configuration S3 est incomplète
     */
    public function presignGet(string $objectKey, ?int $expires = null): string
    {
        if (!$this->s3->isConfigured()) {
            throw new \RuntimeException('Configuration S3 incomplète.');
        }

        $region = $this->s3->region();
        $accessKey = $this->s3->accessKey();
        $secretKey = $this->s3->secretKey();
        $expires = max(1, min($expires ?? $this->s3->expiry(), S3Settings::MAX_EXPIRY));

        [$scheme, $host, $basePath] = $this->resolveHost($region, $this->s3->bucket());

        // Chemin canonique : chaque segment encodé RFC3986, les '/' conservés.
        $key = ltrim($objectKey, '/');
        $encodedKey = implode('/', array_map('rawurlencode', explode('/', $key)));
        $canonicalUri = $basePath . '/' . $encodedKey;

        $now = time();
        $amzDate = gmdate('Ymd\THis\Z', $now);
        $dateStamp = gmdate('Ymd', $now);
        $scope = $dateStamp . '/' . $region . '/s3/aws4_request';

        $params = [
            'X-Amz-Algorithm' => 'AWS4-HMAC-SHA256',
            'X-Amz-Credential' => $accessKey . '/' . $scope,
            'X-Amz-Date' => $amzDate,
            'X-Amz-Expires' => (string) $expires,
            'X-Amz-SignedHeaders' => 'host',
        ];
        ksort($params);
        $canonicalQuery = $this->buildCanonicalQuery($params);

        $canonicalRequest = implode("\n", [
            'GET',
            $canonicalUri,
            $canonicalQuery,
            'host:' . $host . "\n",
            'host',
            'UNSIGNED-PAYLOAD',
        ]);

        $stringToSign = implode("\n", [
            'AWS4-HMAC-SHA256',
            $amzDate,
            $scope,
            hash('sha256', $canonicalRequest),
        ]);

        $signingKey = $this->signingKey($secretKey, $dateStamp, $region);
        $signature = hash_hmac('sha256', $stringToSign, $signingKey);

        return $scheme . '://' . $host . $canonicalUri . '?' . $canonicalQuery . '&X-Amz-Signature=' . $signature;
    }

    /**
     * @return array{0: string, 1: string, 2: string} [scheme, host, basePath]
     */
    private function resolveHost(string $region, string $bucket): array
    {
        $endpoint = $this->s3->endpoint();
        if ($endpoint === '') {
            // AWS, style virtual-hosted : la clé est seule dans le chemin.
            return ['https', $bucket . '.s3.' . $region . '.amazonaws.com', ''];
        }

        // Endpoint personnalisé (S3-compatible), style path : /{bucket}/{clé}.
        $parts = parse_url($endpoint);
        $scheme = $parts['scheme'] ?? 'https';
        $host = (string) ($parts['host'] ?? '');
        if (isset($parts['port'])) {
            $host .= ':' . $parts['port'];
        }

        return [$scheme, $host, '/' . rawurlencode($bucket)];
    }

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

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

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

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