<?php

namespace App\Services\ScssCdn;

use App\Models\Products\Product;
use App\Models\ScssCdn\ScssCdnBundle;
use Illuminate\Support\Facades\Storage;

class ScssCdnStorageService
{
    /**
     * Ruta base para almacenar archivos SCSS CDN
     */
    private const BASE_PATH = 'SCSS_CDN';

    /**
     * Obtiene la ruta del directorio para un producto y bundle
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @return string
     */
    private static function getDirectoryPath(Product $product, $bundle): string
    {
        $versionString = $bundle instanceof ScssCdnBundle ? $bundle->version : $bundle;
        return self::BASE_PATH . '/' . $product->slug . '/' . $versionString;
    }

    /**
     * Obtiene la ruta completa del archivo respetando estructura de carpetas
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @param string $relativePath Ruta relativa desde la raíz del ZIP
     * @return string
     */
    private static function getFilePath(Product $product, $bundle, string $relativePath): string
    {
        $directory = self::getDirectoryPath($product, $bundle);
        // Normalizar separadores de ruta
        $relativePath = str_replace('\\', '/', $relativePath);
        // Asegurar que no empiece con /
        $relativePath = ltrim($relativePath, '/');

        self::exigirQueNoSalgaDelBundle($relativePath);

        return $directory . '/' . $relativePath;
    }

    /**
     * Que la ruta de un fichero **no se salga del directorio de su bundle**.
     *
     * **Esto se pudo explotar y está reproducido.** El nombre de cada entrada del ZIP
     * se normalizaba de separadores y de la barra inicial, pero **nunca de los `..`**.
     * Así que una entrada llamada `a/../../../otro-producto/2026010100/main.scss` se
     * guardaba tal cual, y Flysystem la resolvía a un sitio **dentro del disco pero
     * fuera del bundle**: subiendo un ZIP al producto X se sobrescribía el SCSS del
     * bundle de otro producto, y a partir de ahí ese CSS manipulado se le sirve al
     * Moodle de otro cliente.
     *
     * **Flysystem no lo tapaba, y por qué es fácil creer que sí**: su
     * `PathTraversalDetected` salta cuando la ruta escapa de la **raíz del disco**, no
     * de la carpeta que nosotros consideramos el límite. Con cuatro `..` desde
     * `SCSS_CDN/x/2026010100/a` se llega a la raíz sin salirse de ella, así que para
     * Flysystem es una ruta perfectamente válida. Un `..` de más devolvía `false`; el
     * número justo escribía. El límite lo tiene que poner quien lo define, y eso somos
     * nosotros.
     *
     * Va aquí, en la construcción de la ruta, y no solo al validar el ZIP: así protege a
     * **todas** las puertas —la subida, el editor de un fichero, lo que se añada— y no
     * solo a la que se conocía el día del arreglo.
     *
     * @throws \InvalidArgumentException
     */
    private static function exigirQueNoSalgaDelBundle(string $relativePath): void
    {
        // Se rechaza **cualquier** `..` como segmento, no solo los que escapan de
        // verdad: un `a/../b.scss` es inofensivo y también es una forma rarísima de
        // escribir una ruta en un ZIP de estilos. Contar niveles para permitir los
        // inocentes añade una aritmética que hay que acertar, y aquí no se gana nada.
        foreach (explode('/', $relativePath) as $segmento) {
            if ($segmento === '..') {
                throw new \InvalidArgumentException(
                    'La ruta del archivo no puede salir de su bundle: ' . $relativePath
                );
            }
        }
    }

    /**
     * Que el contenido que se guarda **no lleve una dirección firmada dentro**.
     *
     * Este es el cierre de la bomba de relojería que había, y merece el detalle porque
     * el fallo que evita es de los que aparecen **un año después** de haberlo causado.
     *
     * El SCSS de un bundle se guarda **como lo escribió su autor**, con sus `@import`
     * tal cual. La sustitución por direcciones firmadas se hace **al servir**, en
     * memoria y en cada petición (`processImportsForApi()`), y con una caducidad corta:
     * así lo que recibe un cliente son direcciones acabadas de firmar y nada envejece.
     *
     * **Lo que había.** Tres funciones —`replaceImports()`, `processBundle()` y
     * `processFile()`— sustituían los `@import` y **guardaban el resultado** aquí, con
     * la caducidad por defecto de **365 días**. Un bundle procesado con eso se queda con
     * direcciones escritas dentro que **mueren al año**: a partir de ese día el Moodle
     * del cliente pide cada `@import` y recibe un 403, y su theme se queda sin la mitad
     * de los estilos. Todos los bundles procesados el mismo día caducarían el mismo día.
     * Se borraron el 2026-09-08 —nadie las llamaba y encima usaban columnas que no
     * existen—, pero borrar las tres funciones no impide que alguien vuelva a escribirlo.
     * Esto sí.
     *
     * **Y caza además el caso humano**: un autor que copia del panel el enlace firmado de
     * un fichero y lo pega como `@import url(...)` en su SCSS está armando la misma
     * bomba, con la caducidad del panel. El mensaje le dice qué escribir en su lugar.
     *
     * @throws \InvalidArgumentException
     */
    private static function exigirQueNoLleveUnaUrlFirmada(string $content): void
    {
        // La ruta del CDN **y** una firma: las dos cosas juntas. Solo la ruta podría
        // aparecer en un comentario explicando cómo funciona esto, y eso es inofensivo.
        if (str_contains($content, '/cdn/scss/') && str_contains($content, 'signature=')) {
            throw new \InvalidArgumentException(
                'El contenido que se intenta guardar lleva una dirección firmada del CDN '
                . 'dentro. Eso caduca, y el día que caduque el theme del cliente se queda '
                . 'sin esos estilos sin que falle nada visible. El SCSS se guarda con sus '
                . '@import normales —@import \'variables\';— y el Manager los sustituye por '
                . 'direcciones recién firmadas cada vez que sirve el bundle.'
            );
        }
    }

    /**
     * Guarda un archivo SCSS en el storage respetando estructura de carpetas
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @param string $relativePath Ruta relativa desde la raíz del ZIP
     * @param string $content
     * @return bool
     */
    public static function saveFile(Product $product, $bundle, string $relativePath, string $content): bool
    {
        // **Fuera del `try`, y a propósito.** El `catch (\Exception)` de abajo está para
        // los fallos de entrada/salida —disco lleno, permisos—, que se registran y
        // devuelven `false`. El rechazo de una ruta que sale del bundle no es un fallo de
        // disco: es una ruta que nadie debería haber intentado escribir, y tragárselo en
        // un `false` lo convierte en «un archivo que no se pudo subir» entre otros. Que
        // suba.
        $filePath = self::getFilePath($product, $bundle, $relativePath);
        self::exigirQueNoLleveUnaUrlFirmada($content);

        try {
            $directory = dirname($filePath);

            // Crear directorio si no existe
            if (!Storage::exists($directory)) {
                Storage::makeDirectory($directory);
            }

            // Guardar archivo
            return Storage::put($filePath, $content) !== false;
        } catch (\Exception $e) {
            \Log::error('Error al guardar archivo SCSS CDN: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Lee un archivo SCSS del storage
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @param string $relativePath Ruta relativa desde la raíz del ZIP
     * @return string|null
     */
    public static function getFile(Product $product, $bundle, string $relativePath): ?string
    {
        try {
            $filePath = self::getFilePath($product, $bundle, $relativePath);

            if (!Storage::exists($filePath)) {
                return null;
            }

            return Storage::get($filePath);
        } catch (\Exception $e) {
            \Log::error('Error al leer archivo SCSS CDN: ' . $e->getMessage());
            return null;
        }
    }

    /**
     * Elimina un archivo SCSS del storage
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @param string $relativePath Ruta relativa desde la raíz del ZIP
     * @return bool
     */
    public static function deleteFile(Product $product, $bundle, string $relativePath): bool
    {
        try {
            $filePath = self::getFilePath($product, $bundle, $relativePath);

            if (!Storage::exists($filePath)) {
                return true; // Ya no existe, consideramos éxito
            }

            return Storage::delete($filePath);
        } catch (\Exception $e) {
            \Log::error('Error al eliminar archivo SCSS CDN: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Lista todos los archivos de un bundle manteniendo estructura
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @return array Array con estructura de archivos ['relative_path' => 'content', ...]
     */
    public static function listFiles(Product $product, $bundle): array
    {
        try {
            $directory = self::getDirectoryPath($product, $bundle);

            if (!Storage::exists($directory)) {
                return [];
            }

            $files = Storage::allFiles($directory);
            $result = [];

            foreach ($files as $file) {
                // Obtener ruta relativa desde el directorio del bundle
                $relativePath = str_replace($directory . '/', '', $file);
                $content = Storage::get($file);
                $result[$relativePath] = $content;
            }

            return $result;
        } catch (\Exception $e) {
            \Log::error('Error al listar archivos SCSS CDN: ' . $e->getMessage());
            return [];
        }
    }

    /**
     * Verifica si existe un archivo
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @param string $relativePath Ruta relativa desde la raíz del ZIP
     * @return bool
     */
    public static function fileExists(Product $product, $bundle, string $relativePath): bool
    {
        try {
            $filePath = self::getFilePath($product, $bundle, $relativePath);
            return Storage::exists($filePath);
        } catch (\Exception $e) {
            \Log::error('Error al verificar archivo SCSS CDN: ' . $e->getMessage());
            return false;
        }
    }

    /**
     * Elimina todos los archivos de un bundle
     *
     * @param Product $product
     * @param ScssCdnBundle|string $bundle
     * @return bool
     */
    public static function deleteBundle(Product $product, $bundle): bool
    {
        try {
            $directory = self::getDirectoryPath($product, $bundle);

            if (!Storage::exists($directory)) {
                return true; // Ya no existe, consideramos éxito
            }

            return Storage::deleteDirectory($directory);
        } catch (\Exception $e) {
            \Log::error('Error al eliminar bundle SCSS CDN: ' . $e->getMessage());
            return false;
        }
    }
}

