<?php

namespace App\Services\ScssCdn;

use ZipArchive;
use Illuminate\Support\Facades\Log;

class ScssCdnZipService
{
    /**
     * Tamaño máximo del archivo ZIP (50MB)
     */
    /**
     * El tope de fábrica, en bytes.
     *
     * **El que se aplica sale de Ajustes del Manager**: este es el que queda si el
     * ajuste no está sembrado. Estaba escrito aquí y otra vez en `LimiteDeSubida`, con
     * el mismo 50 en los dos sitios — el patrón que ya se pagó una vez: uno cambia, el
     * otro no, y la pantalla anuncia un tope que el servidor no respeta.
     */
    private const MAX_FILE_SIZE = 50 * 1024 * 1024;

    /** El tope que se aplica de verdad. */
    private static function topeEnBytes(): int
    {
        return app(\App\Services\System\Settings::class)->zipDeBundleEnBytes();
    }

    /**
     * Descomprime un archivo ZIP y retorna estructura de archivos
     *
     * @param string $zipPath Ruta al archivo ZIP
     * @return array Array con estructura ['relative_path' => 'content', ...]
     * @throws \Exception
     */
    public static function extractZip(string $zipPath): array
    {
        if (!file_exists($zipPath)) {
            throw new \Exception('El archivo ZIP no existe.');
        }

        // Validar tamaño
        $fileSize = filesize($zipPath);
        if ($fileSize > self::topeEnBytes()) {
            throw new \Exception('El archivo ZIP excede el tamaño máximo de ' . round(self::topeEnBytes() / 1048576) . ' MB.');
        }

        $zip = new ZipArchive();
        $result = $zip->open($zipPath);

        if ($result !== true) {
            throw new \Exception('No se pudo abrir el archivo ZIP. Código de error: ' . $result);
        }

        $files = [];
        $invalidFiles = [];

        // Validar estructura antes de extraer
        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entryName = $zip->getNameIndex($i);
            
            // Ignorar directorios
            if (substr($entryName, -1) === '/') {
                continue;
            }

            // **Una entrada que apunta fuera de su carpeta invalida el ZIP entero.**
            // Reproducido: `a/../../../otro-producto/2026010100/main.scss` escribía en
            // el bundle de otro producto, y ese CSS se le sirve al Moodle de otro
            // cliente. No se ignora la entrada y se sigue: un ZIP con una ruta así no
            // es un descuido de quien lo comprimió, y lo que hay que hacer con él es no
            // aceptar nada.
            if (self::seSaleDeSuCarpeta($entryName)) {
                $zip->close();

                throw new \Exception(
                    'El ZIP contiene una ruta que sale de su carpeta (' . $entryName . '). '
                    . 'No se ha subido nada.'
                );
            }

            // Validar que sea archivo .scss
            $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
            if ($extension !== 'scss') {
                $invalidFiles[] = $entryName;
                continue;
            }

            // Leer contenido del archivo
            $content = $zip->getFromIndex($i);
            if ($content === false) {
                Log::warning("No se pudo leer el contenido del archivo: {$entryName}");
                continue;
            }

            // Normalizar ruta (usar / como separador)
            $normalizedPath = str_replace('\\', '/', $entryName);
            $files[$normalizedPath] = $content;
        }

        $zip->close();

        // Si hay archivos inválidos, lanzar excepción
        if (!empty($invalidFiles)) {
            throw new \Exception(
                'El ZIP contiene archivos que no son .scss: ' . implode(', ', array_slice($invalidFiles, 0, 5)) .
                (count($invalidFiles) > 5 ? ' y ' . (count($invalidFiles) - 5) . ' más.' : '')
            );
        }

        if (empty($files)) {
            throw new \Exception('El archivo ZIP no contiene archivos .scss válidos.');
        }

        return $files;
    }

    /**
     * Valida la estructura del ZIP
     *
     * @param string $zipPath Ruta al archivo ZIP
     * @return array ['valid' => bool, 'errors' => array, 'file_count' => int]
     */
    public static function validateZipStructure(string $zipPath): array
    {
        $errors = [];
        $fileCount = 0;

        if (!file_exists($zipPath)) {
            return [
                'valid' => false,
                'errors' => ['El archivo ZIP no existe.'],
                'file_count' => 0,
            ];
        }

        // Validar tamaño
        $fileSize = filesize($zipPath);
        if ($fileSize > self::topeEnBytes()) {
            $errors[] = 'El archivo ZIP excede el tamaño máximo de ' . round(self::topeEnBytes() / 1048576) . ' MB.';
        }

        $zip = new ZipArchive();
        $result = $zip->open($zipPath);

        if ($result !== true) {
            $errors[] = 'No se pudo abrir el archivo ZIP.';
            return [
                'valid' => false,
                'errors' => $errors,
                'file_count' => 0,
            ];
        }

        $invalidFiles = [];
        $scssFiles = [];

        for ($i = 0; $i < $zip->numFiles; $i++) {
            $entryName = $zip->getNameIndex($i);
            
            // Ignorar directorios
            if (substr($entryName, -1) === '/') {
                continue;
            }

            $fileCount++;

            // La misma comprobación que en la extracción, aquí para que el error salga
            // en la validación previa —que es lo que lee la pantalla— y no como una
            // excepción a medio subir.
            if (self::seSaleDeSuCarpeta($entryName)) {
                $errors[] = 'El ZIP contiene una ruta que sale de su carpeta: ' . $entryName
                    . '. Vuelve a comprimir seleccionando los archivos, no la carpeta.';
                continue;
            }

            $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
            
            if ($extension === 'scss') {
                $scssFiles[] = $entryName;
            } else {
                $invalidFiles[] = $entryName;
            }
        }

        $zip->close();

        if (!empty($invalidFiles)) {
            $errors[] = 'El ZIP contiene archivos que no son .scss: ' . count($invalidFiles) . ' archivo(s).';
        }

        if (empty($scssFiles)) {
            $errors[] = 'El archivo ZIP no contiene archivos .scss válidos.';
        }

        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'file_count' => $fileCount,
            'scss_count' => count($scssFiles),
        ];
    }

    /**
     * Procesa archivos extraídos y crea estructura de datos
     *
     * @param array $extractedFiles Array con estructura ['relative_path' => 'content', ...]
     * @return array Array procesado con información adicional
     */
    /**
     * ¿Esta entrada del ZIP apunta fuera de su propia carpeta?
     *
     * Un `..` como segmento, o una ruta absoluta —de Unix o con letra de unidad de
     * Windows—. Se mira sobre el nombre **tal como viene en el ZIP**, antes de
     * normalizar nada: es donde se puede decidir que el fichero entero no vale.
     */
    private static function seSaleDeSuCarpeta(string $entryName): bool
    {
        $ruta = str_replace('\\\\', '/', $entryName);

        if (str_starts_with($ruta, '/') || preg_match('#^[A-Za-z]:/#', $ruta) === 1) {
            return true;
        }

        return in_array('..', explode('/', $ruta), true);
    }

    public static function processExtractedFiles(array $extractedFiles): array
    {
        $processed = [];

        foreach ($extractedFiles as $relativePath => $content) {
            // Normalizar ruta
            $normalizedPath = str_replace('\\', '/', $relativePath);
            $normalizedPath = ltrim($normalizedPath, '/');

            // Extraer nombre del archivo
            $filename = basename($normalizedPath);
            
            // Extraer directorio
            $directory = dirname($normalizedPath);
            if ($directory === '.') {
                $directory = '';
            }

            $processed[] = [
                'relative_path' => $normalizedPath,
                'filename' => $filename,
                'directory' => $directory,
                'content' => $content,
                'size' => strlen($content),
            ];
        }

        // Ordenar por ruta
        usort($processed, function ($a, $b) {
            return strcmp($a['relative_path'], $b['relative_path']);
        });

        return $processed;
    }
}

