<?php

namespace App\Support;

/**
 * Cuánto pesa como máximo un fichero que se puede subir de verdad.
 *
 * **El problema que resuelve** (PRODSECU-156): los formularios de imagen anunciaban «5 MB»
 * porque el número estaba escrito a mano en el mensaje de validación, y ese es el límite de
 * **la última de cuatro capas**. Delante están el servidor web, el PHP de la máquina y
 * Livewire, y cualquiera de ellas puede cortar antes. El resultado fue un ticket que decía
 * *«el aviso dice 5MB pero al subir una de 3 no se pudo subir»*: el aviso no mentía por
 * error de cálculo, mentía porque **no sabía de las otras capas**.
 *
 * Aquí el número se calcula. Lo que se anuncia es lo que se aplica.
 *
 * **Qué se puede saber y qué no.** Esto es importante y es el techo de lo que se puede
 * arreglar desde PHP:
 *
 * | Capa | ¿La ve PHP? |
 * |---|---|
 * | Validación de Laravel (`max:5120`) | Sí, la ponemos nosotros |
 * | `upload_max_filesize` | Sí, `ini_get()` |
 * | `post_max_size` | Sí, `ini_get()` |
 * | `client_max_body_size` de nginx | **No.** Cuando nginx corta, **PHP no se ejecuta**: no hay dónde leerlo ni dónde ejecutar código |
 *
 * Así que este cálculo cubre tres de las cuatro. La cuarta solo se puede detectar **desde
 * el navegador**, cuando la subida se va con un 413 —lo hace `resources/js/subidas.js`— y
 * avisando en el despliegue, que es para lo que está la tabla de
 * `operations/deploy-y-cicd.md`.
 */
class LimiteDeSubida
{
    /**
     * El tope que declara la validación, en kilobytes.
     *
     * Es el mismo `max:5120` que llevan los cuatro formularios de imagen. Vive aquí para
     * que el número exista **una vez**: escrito en la regla y otra vez en el mensaje, es
     * cuestión de tiempo que uno cambie y el otro no.
     */
    public const IMAGENES_KB = 5120;

    /**
     * Los documentos de Recursos: PDF, Word, Excel, PowerPoint.
     *
     * El doble que una imagen, porque un PDF con capturas los pasa sin esfuerzo.
     */
    public const DOCUMENTOS_KB = 10240;

    /**
     * Un YAML de configuración de Setup.
     *
     * Es texto: 10 MB son cientos de miles de líneas. El tope está para que no se
     * suba un fichero equivocado, no porque haga falta tanto.
     */
    public const YAML_KB = 10240;

    /**
     * Y el de los ZIP de SCSS-CDN, que es otro orden de magnitud.
     *
     * **Es el valor de fábrica, no el que se aplica.** El que manda sale de Ajustes del
     * Manager (`Settings::SCSSCDN_ZIP_MB`) y se lee con `zipDeBundleEnKb()`: este número
     * es lo que se usa si el ajuste no está sembrado. Se deja como constante porque es
     * el defecto de la migración y el de los tests.
     */
    public const ZIP_KB = 51200;

    /**
     * El límite real, en kilobytes: el menor de los tres que PHP puede ver.
     *
     * Se resta un poco al `post_max_size` **a propósito**: el cuerpo de la petición no es
     * solo el fichero —lleva los demás campos del formulario, las cabeceras y el sobre
     * multipart—, así que con el tope justo, un fichero de exactamente ese tamaño se
     * rechaza. El margen es de 512 KB, que sobra para cualquier formulario del panel.
     */
    public static function enKb(int $topeDeLaValidacion = self::IMAGENES_KB): int
    {
        $candidatos = [$topeDeLaValidacion];

        $subida = self::iniEnKb('upload_max_filesize');
        if ($subida !== null) {
            $candidatos[] = $subida;
        }

        $post = self::iniEnKb('post_max_size');
        if ($post !== null) {
            $candidatos[] = max(1, $post - 512);
        }

        return min($candidatos);
    }

    /**
     * El límite dicho como se lee: «5 MB», «1,5 MB», «800 KB», «2 GB».
     */
    public static function comoTexto(int $topeDeLaValidacion = self::IMAGENES_KB): string
    {
        return self::formatear(self::enKb($topeDeLaValidacion));
    }

    /**
     * Un tamaño en kilobytes, dicho como se lee.
     *
     * **Sube a GB a partir de 1024 MB, y eso no es cosmética.** En español el punto es
     * separador de millar, así que un `2.048 MB` —que son 2 GB— se lee como «2 MB»: el
     * número dice justo lo contrario de lo que vale. Apareció en la pantalla de
     * Configuraciones, que existe **precisamente para aclarar los límites**, y ahí un
     * número ambiguo es peor que no poner nada.
     *
     * Y está aquí, público, porque la vista de Configuraciones tenía **su propia copia**
     * de este cálculo. Dos formateos del mismo dato es la forma segura de que uno de los
     * dos se quede sin arreglar.
     *
     * Coma decimal, y sin decimales cuando es redondo.
     */
    public static function formatear(int $kb): string
    {
        if ($kb < 1024) {
            return $kb . ' KB';
        }

        $mb = $kb / 1024;

        if ($mb < 1024) {
            return self::sinCerosSobrantes($mb) . ' MB';
        }

        return self::sinCerosSobrantes($mb / 1024) . ' GB';
    }

    private static function sinCerosSobrantes(float $numero): string
    {
        return rtrim(rtrim(number_format($numero, 1, ',', ''), '0'), ',');
    }

    /**
     * ¿Hay alguna capa por debajo del tope que quiere el formulario?
     *
     * Cuando esto es `true`, el formulario **no puede cumplir su promesa** y hay que
     * decirlo donde se ve, no dejarlo para cuando alguien intente subir un fichero grande.
     * Es una cuestión de configuración del servidor, así que la frase apunta a Sistemas.
     */
    public static function loRecortaElServidor(int $topeDeLaValidacion = self::IMAGENES_KB): bool
    {
        return self::enKb($topeDeLaValidacion) < $topeDeLaValidacion;
    }

    /**
     * Los cuatro números, para poder diagnosticar sin entrar por SSH.
     *
     * `client_max_body_size` aparece con `null` **a propósito y no es un hueco**: es la
     * respuesta correcta, porque PHP no lo puede saber. Un `?` en esa fila es información
     * —dice dónde hay que mirar— y poner un número inventado sería peor.
     *
     * @return array<string, mixed>
     */
    public static function diagnostico(int $topeDeLaValidacion = self::IMAGENES_KB): array
    {
        return [
            'validacion_kb' => $topeDeLaValidacion,
            'upload_max_filesize_kb' => self::iniEnKb('upload_max_filesize'),
            'post_max_size_kb' => self::iniEnKb('post_max_size'),
            // No es medible desde aquí. Ver el docblock de la clase.
            'client_max_body_size_kb' => null,
            'efectivo_kb' => self::enKb($topeDeLaValidacion),
            'lo_recorta_el_servidor' => self::loRecortaElServidor($topeDeLaValidacion),
        ];
    }

    /**
     * Valores de `php.ini` fingidos, solo para pruebas.
     *
     * **Hace falta porque estos dos ajustes no se pueden cambiar en caliente**:
     * `upload_max_filesize` y `post_max_size` son `PHP_INI_PERDIR`, así que `ini_set()`
     * devuelve `false` y las ramas interesantes —el servidor más estricto que el
     * formulario, que es **el caso del ticket**— se quedaban sin probar.
     *
     * Mismo patrón que `Carbon::setTestNow()`, que la suite ya usa: se finge, se prueba y
     * se deja como estaba.
     *
     * @var array<string, string>|null
     */
    private static ?array $fingidos = null;

    /**
     * @param  array<string, string>  $valores  p. ej. `['upload_max_filesize' => '2M']`
     */
    public static function fingirIni(array $valores): void
    {
        self::$fingidos = $valores;
    }

    public static function dejarDeFingir(): void
    {
        self::$fingidos = null;
    }

    /**
     * Un valor de `php.ini` de tamaño, en kilobytes.
     *
     * Acepta las formas que usa PHP —`8M`, `2G`, `512K`, un número suelto en bytes— y
     * devuelve `null` cuando **no hay límite**: `0` o vacío significan «sin tope» en
     * `upload_max_filesize` y `post_max_size`, y tratarlo como un tope de 0 dejaría el
     * límite efectivo en nada.
     */
    private static function iniEnKb(string $clave): ?int
    {
        $valor = self::$fingidos !== null
            ? trim((string) (self::$fingidos[$clave] ?? ''))
            : trim((string) ini_get($clave));

        if ($valor === '' || $valor === '0') {
            return null;
        }

        $unidad = strtoupper(substr($valor, -1));
        $numero = (float) $valor;

        $bytes = match ($unidad) {
            'G' => $numero * 1024 * 1024 * 1024,
            'M' => $numero * 1024 * 1024,
            'K' => $numero * 1024,
            default => $numero,
        };

        return (int) floor($bytes / 1024);
    }
}
