<?php

declare(strict_types=1);

namespace ApiModel\Infrastructure\Storage;

use ApiModel\Infrastructure\Persistence\Database;
use DateTimeImmutable;
use DateTimeInterface;
use InvalidArgumentException;
use RuntimeException;

/**
 * Reserva rutas de disco para subidas locales, agrupadas por carpetas lógicas y por día {@see self::DATE_FORMAT} (estilo clave R2 por prefijo/fecha).
 * No escribe el archivo: solo calcula rutas y opcionalmente crea directorios.
 */
final class LocalDatedUploadStorage
{
    public const DATE_FORMAT = 'Ymd';

    private function __construct(
        private readonly string $rootAbsolute,
    ) {
    }

    public static function isConfigured(): bool
    {
        return trim((string) ($_ENV['LOCAL_UPLOAD_ROOT'] ?? '')) !== '';
    }

    /**
     * @param string $apiRoot Directorio raíz del proyecto api (donde está composer.json)
     */
    public static function tryFromEnv(string $apiRoot): ?self
    {
        $raw = trim((string) ($_ENV['LOCAL_UPLOAD_ROOT'] ?? ''));
        if ($raw === '') {
            return null;
        }

        $root = self::normalizeRootPath($raw, $apiRoot);

        return new self($root);
    }

    /**
     * @throws RuntimeException si LOCAL_UPLOAD_ROOT no está definido
     */
    public static function fromEnv(string $apiRoot): self
    {
        $i = self::tryFromEnv($apiRoot);
        if ($i === null) {
            throw new RuntimeException(
                'Almacenamiento local no configurado: define LOCAL_UPLOAD_ROOT (ruta absoluta o relativa al directorio api/).',
            );
        }

        return $i;
    }

    public function rootAbsolute(): string
    {
        return $this->rootAbsolute;
    }

    /**
     * @param list<string> $logicalSegments Carpetas bajo la raíz antes del directorio de fecha (p. ej. "claims", uuid del reclamo).
     * @param bool         $createDirectories Crea el directorio destino con mkdir recursivo.
     *
     * @return LocalUploadAllocation ruta relativa con barras / (adecuada para documents.storage_path)
     */
    public function allocate(
        string $originalFilename,
        array $logicalSegments = [],
        ?DateTimeInterface $day = null,
        bool $createDirectories = true,
    ): LocalUploadAllocation {
        $day = $day ?? new DateTimeImmutable('today');
        $dateFolder = $day->format(self::DATE_FORMAT);
        if (! preg_match('/^\d{8}$/', $dateFolder)) {
            throw new RuntimeException('Carpeta de fecha inválida.');
        }

        $prefix = trim((string) ($_ENV['LOCAL_UPLOAD_TOP_SEGMENT'] ?? ''));
        $segments = [];
        if ($prefix !== '') {
            $segments[] = self::sanitizeSingleSegment($prefix);
        }
        foreach ($logicalSegments as $seg) {
            $segments[] = self::sanitizeSingleSegment($seg);
        }
        $segments[] = $dateFolder;

        $storedName = self::buildStoredBasename($originalFilename);
        $relative = implode('/', array_merge($segments, [$storedName]));
        $absoluteFile = $this->rootAbsolute . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative);
        $absoluteDir = dirname($absoluteFile);

        if ($createDirectories && ! is_dir($absoluteDir)) {
            if (! mkdir($absoluteDir, 0755, true) && ! is_dir($absoluteDir)) {
                throw new RuntimeException('No se pudo crear el directorio: ' . $absoluteDir);
            }
        }

        return new LocalUploadAllocation(
            relativePath: $relative,
            absoluteFilePath: $absoluteFile,
            absoluteDirectory: $absoluteDir,
            dateFolder: $dateFolder,
            storedBasename: $storedName,
        );
    }

    /**
     * Resuelve una clave relativa guardada en BD hacia ruta absoluta bajo la misma raíz.
     */
    public function absolutePathForRelative(string $relativePath): string
    {
        $relativePath = trim(str_replace('\\', '/', $relativePath), '/');
        if ($relativePath === '' || str_contains($relativePath, '..')) {
            throw new InvalidArgumentException('Ruta relativa inválida.');
        }

        return $this->rootAbsolute . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
    }

    private static function normalizeRootPath(string $raw, string $apiRoot): string
    {
        $trimmed = trim($raw);
        if ($trimmed === '') {
            throw new InvalidArgumentException('LOCAL_UPLOAD_ROOT vacío.');
        }
        $path = str_replace('/', DIRECTORY_SEPARATOR, $trimmed);
        $isAbsolute = (strlen($path) >= 3 && ctype_alpha($path[0]) && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/'))
            || str_starts_with($path, DIRECTORY_SEPARATOR);
        $full = $isAbsolute ? $path : $apiRoot . DIRECTORY_SEPARATOR . $path;
        $full = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $full), DIRECTORY_SEPARATOR);

        if (is_dir($full)) {
            $rp = realpath($full);
            if ($rp !== false) {
                return $rp;
            }
        }

        return $full;
    }

    private static function sanitizeSingleSegment(string $segment): string
    {
        $segment = trim($segment);
        if ($segment === '' || str_contains($segment, '..')) {
            throw new InvalidArgumentException('Segmento de ruta inválido.');
        }
        if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/', $segment)) {
            throw new InvalidArgumentException('Segmento de ruta inválido: ' . $segment);
        }

        return $segment;
    }

    private static function buildStoredBasename(string $originalFilename): string
    {
        $base = basename(str_replace('\\', '/', $originalFilename));
        $base = preg_replace('/[^a-zA-Z0-9._-]+/', '_', $base) ?? '';
        $base = trim((string) $base, '._- ');
        if ($base === '') {
            $base = 'file';
        }
        if (strlen($base) > 120) {
            $base = substr($base, 0, 120);
        }

        $dot = strrpos($base, '.');
        $ext = '';
        if ($dot !== false && $dot < strlen($base) - 1) {
            $ext = strtolower(substr($base, $dot + 1));
            $ext = preg_replace('/[^a-z0-9]/', '', $ext) ?? '';
            if (strlen($ext) > 10) {
                $ext = substr($ext, 0, 10);
            }
            $base = substr($base, 0, $dot);
        }

        $id = Database::uuid();
        if ($ext !== '') {
            return $id . '.' . $ext;
        }

        return $id;
    }
}
