<?php

declare(strict_types=1);

namespace ApiModel\Infrastructure\Storage;

use InvalidArgumentException;
use RuntimeException;

/**
 * URLs firmadas para Cloudflare R2 (API compatible S3, SigV4 en query string).
 * Sin aws-sdk-php: {@see S3SigV4QueryPresigner}.
 */
final class R2S3Presigner
{
    private function __construct(
        private readonly S3SigV4QueryPresigner $presigner,
        private readonly string $bucket,
        private readonly string $publicBaseUrl,
    ) {
    }

    public static function isConfigured(): bool
    {
        return self::nonEmptyEnv('R2_ACCESS_KEY_ID')
            && self::nonEmptyEnv('R2_SECRET_ACCESS_KEY')
            && self::nonEmptyEnv('R2_ENDPOINT')
            && self::nonEmptyEnv('R2_BUCKET');
    }

    /**
     * @throws RuntimeException si la configuración es inválida
     */
    public static function createFromEnv(): self
    {
        $instance = self::tryCreate();
        if ($instance === null) {
            throw new RuntimeException(
                'R2 no configurado: definen R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT y R2_BUCKET.',
            );
        }

        return $instance;
    }

    public static function tryCreate(): ?self
    {
        if (! self::isConfigured()) {
            return null;
        }

        $accessKeyId = self::requiredEnv('R2_ACCESS_KEY_ID');
        $secretAccessKey = self::requiredEnv('R2_SECRET_ACCESS_KEY');
        $endpoint = self::requiredEnv('R2_ENDPOINT');
        $bucket = self::requiredEnv('R2_BUCKET');
        $region = self::envString('R2_REGION', 'auto');
        $publicBaseUrl = rtrim(self::envString('R2_PUBLIC_BASE_URL', ''), '/');

        if (! preg_match('#^https?://#i', $endpoint)) {
            throw new InvalidArgumentException(
                "R2_ENDPOINT inválido. Debe ser una URL tipo https://<ACCOUNT_ID>.r2.cloudflarestorage.com (valor actual: {$endpoint}).",
            );
        }

        $presigner = new S3SigV4QueryPresigner($accessKeyId, $secretAccessKey, $region, $endpoint);

        return new self($presigner, $bucket, $publicBaseUrl);
    }

    /**
     * @return array{url:string,method:string,headers:array<string,string>,key:string,expiresIn:int}
     */
    public function createUploadUrl(string $key, string $contentType, int $expiresInSeconds = 900): array
    {
        $key = $this->sanitizeObjectKey($key);
        $expiresInSeconds = $this->clampExpires($expiresInSeconds);

        $signed = $this->presigner->presign(
            'PUT',
            $this->bucket,
            $key,
            $expiresInSeconds,
            ['content-type' => $contentType],
        );

        return [
            'url' => $signed['url'],
            'method' => $signed['method'],
            'headers' => [
                'Content-Type' => $contentType,
            ],
            'key' => $key,
            'expiresIn' => $expiresInSeconds,
        ];
    }

    /**
     * @return array{url:string,method:string,key:string,expiresIn:int,publicUrl?:string}
     */
    public function createReadUrl(string $key, int $expiresInSeconds = 900): array
    {
        $key = $this->sanitizeObjectKey($key);
        $expiresInSeconds = $this->clampExpires($expiresInSeconds);

        $signed = $this->presigner->presign('GET', $this->bucket, $key, $expiresInSeconds);

        $out = [
            'url' => $signed['url'],
            'method' => $signed['method'],
            'key' => $key,
            'expiresIn' => $expiresInSeconds,
        ];

        if ($this->publicBaseUrl !== '') {
            $out['publicUrl'] = $this->publicBaseUrl . '/' . $this->encodeKeyForUrl($key);
        }

        return $out;
    }

    private static function nonEmptyEnv(string $key): bool
    {
        return trim((string) ($_ENV[$key] ?? '')) !== '';
    }

    private static function envString(string $key, string $default = ''): string
    {
        $v = $_ENV[$key] ?? $default;

        return is_string($v) ? $v : (string) $v;
    }

    private static function requiredEnv(string $key): string
    {
        $v = trim((string) ($_ENV[$key] ?? ''));
        if ($v === '') {
            throw new RuntimeException("Variable de entorno obligatoria ausente o vacía: {$key}");
        }

        return $v;
    }

    private function sanitizeObjectKey(string $key): string
    {
        $key = trim($key);
        $key = str_replace('\\', '/', $key);
        $key = preg_replace('#/+#', '/', $key) ?? $key;
        $key = ltrim($key, '/');

        if ($key === '' || str_contains($key, '..')) {
            throw new InvalidArgumentException('Key inválida.');
        }

        return $key;
    }

    private function clampExpires(int $expiresInSeconds): int
    {
        if ($expiresInSeconds < 60) {
            return 60;
        }
        if ($expiresInSeconds > 3600) {
            return 3600;
        }

        return $expiresInSeconds;
    }

    private function encodeKeyForUrl(string $key): string
    {
        $parts = explode('/', $key);
        $parts = array_map(static fn ($p) => rawurlencode($p), $parts);

        return implode('/', $parts);
    }
}
