<?php

namespace App\Services\ScssCdn;

use App\Models\ScssCdn\ScssCdnBundle;
use App\Models\ScssCdn\ScssCdnFile;

/**
 * Qué ficheros de un bundle son puntos de entrada, y marcarlos (MGR-093).
 *
 * **El problema.** Al subir un ZIP, cada fichero se creaba con `is_servable = false` y no
 * había ninguna detección: **nada quedaba servible**. Hasta que alguien marcaba los puntos
 * de entrada a mano, la acción `scss-cdn` respondía `3002` «no hay ficheros servibles», que
 * por `norma-cero-errores.md` **no es un error** — así que no avisaba el Dashboard, ni el
 * visor, ni un correo. Subir el ZIP y marcharse dejaba el bundle **publicado y vacío a
 * efectos prácticos**, con toda la apariencia de estar bien.
 *
 * **Y en el Moodle del cliente tampoco se ve, que es lo peor.** Según el autor del theme:
 * si no le llega ningún fichero, el theme se comporta «como si no tuviera CSS» y **tira
 * de su caché**. O sea que el sitio sigue viéndose bien — con los estilos de antes — y
 * nadie se entera de nada. **Hasta que alguien purgue las cachés de ese Moodle**, que
 * puede ser semanas después y por un motivo que no tiene nada que ver: una
 * actualización, una tarea de mantenimiento. Y entonces el sitio se queda sin estilos, y
 * el bundle que lo provocó se subió hace un mes.
 *
 * Eso es lo que hace que esto **no** sea un detalle de comodidad: el hueco entre la causa
 * y el síntoma se mide en semanas, y quien lo sufre no tiene forma de unirlos.
 *
 * **Y el dato ya estaba calculado**, que es lo que hacía absurdo partir de cero:
 * `ScssCdnImportParser::findFileInBundle()` resuelve a qué fichero apunta cada `@import`
 * —con la convención del guion bajo de los partials, y probando ruta relativa y desde la
 * raíz—, y se usa al servir. Aquí se reutiliza tal cual.
 *
 * **La definición es la del lenguaje, no una heurística nuestra**: un punto de entrada es un
 * fichero **al que nadie importa**. Un fichero de variables o de mixins solo tiene sentido
 * importado desde otro, así que alguien lo importa y queda fuera.
 *
 * **Se propone, no se impone.** El autor puede cambiar las marcas después: la detección
 * acierta casi siempre, pero un bundle puede tener un fichero que sea entrada *y* que
 * además alguien importe, y eso no lo puede saber nadie más que él.
 *
 * ## Y solo marca cuando la respuesta es UNA
 *
 * La primera versión marcaba **todos** los ficheros a los que nadie importa. Se corrigió
 * al saber cómo son los bundles de verdad: en producción, el de `theme_fresk` tiene **un**
 * solo fichero servible, porque el theme apunta a **un** punto de entrada en su
 * configuración.
 *
 * **Y el motivo NO es que un servible de más rompa nada.** Eso se afirmó primero y hubo
 * que corregirlo: el autor del theme dice que él **solo busca los `@import` de su punto
 * de entrada**, así que un fichero servible de más probablemente lo ignore. Lo que hace
 * el Manager es entregarlo en la respuesta; **qué hace el consumidor con lo que le sobra
 * no lo decide esta pantalla, y no lo sabemos.**
 *
 * El motivo es más simple y no depende de creer nada sobre el theme: **dos candidatos
 * casi siempre son un fichero suelto en el ZIP** —una hoja de pruebas, uno que se quedó
 * sin importar al refactorizar—, y eso es algo que quien lo sube quiere saber. Marcarlo
 * en silencio esconde el aviso; no marcar nada y nombrarlos lo enseña.
 *
 * Así que la regla es la misma que ya había para el caso de cero, extendida: **se marca
 * cuando no hay nada que decidir**.
 */
class PuntosDeEntrada
{
    /**
     * Marca como servible **el** fichero al que nadie importa, si hay exactamente uno.
     *
     * **No toca los que ya estuvieran marcados.** Si alguien ajustó las marcas a mano y
     * vuelve a subir el ZIP, la subida no debe deshacérselo: solo añade.
     *
     * @return array{marcados: int, total: int, ninguno: bool, ambiguo: bool, candidatos: list<string>}
     *         `candidatos` solo viene lleno cuando `ambiguo`: son las rutas que habría
     *         que revisar, para poder nombrarlas en la pantalla.
     */
    public static function marcar(ScssCdnBundle $bundle): array
    {
        $ficheros = $bundle->files()->get();

        if ($ficheros->isEmpty()) {
            return ['marcados' => 0, 'total' => 0, 'ninguno' => true, 'ambiguo' => false, 'candidatos' => []];
        }

        $importados = self::importados($bundle, $ficheros);

        $entradas = $ficheros->reject(
            fn (ScssCdnFile $fichero) => in_array($fichero->id, $importados, true)
        );

        // **Si no sale ninguno, no se marca nada.** Pasa cuando todos se importan entre sí
        // —un ciclo— o cuando el bundle es todo partials. Elegir uno a dedo ahí sería
        // adivinar, y adivinar es peor que decir que no se sabe: quien suba el bundle tiene
        // que verlo y decidir.
        if ($entradas->isEmpty()) {
            return [
                'marcados' => 0,
                'total' => $ficheros->count(),
                'ninguno' => true,
                'ambiguo' => false,
                'candidatos' => [],
            ];
        }

        // **Más de uno: no se marca ninguno y se dicen cuáles son.** Un theme apunta a
        // un punto de entrada, así que dos candidatos casi siempre significan que en el
        // ZIP hay un fichero que nadie importa y que no debería estar — y eso quien lo
        // sube quiere saberlo. No es que un servible de más rompa el theme: es que
        // marcarlo en silencio esconde el aviso.
        if ($entradas->count() > 1) {
            return [
                'marcados' => 0,
                'total' => $ficheros->count(),
                'ninguno' => false,
                'ambiguo' => true,
                'candidatos' => $entradas
                    ->sortBy('relative_path')
                    ->pluck('relative_path')
                    ->values()
                    ->all(),
            ];
        }

        $entrada = $entradas->first();
        $marcados = 0;

        // Ya marcado a mano: no hay nada que hacer, y no se cuenta como marcado por
        // nosotros.
        if (! $entrada->is_servable) {
            $entrada->update(['is_servable' => true]);
            $marcados = 1;
        }

        return [
            'marcados' => $marcados,
            'total' => $ficheros->count(),
            'ninguno' => false,
            'ambiguo' => false,
            'candidatos' => [],
        ];
    }

    /**
     * Los ids de los ficheros que algún otro del bundle importa.
     *
     * @param  \Illuminate\Support\Collection<int, ScssCdnFile>  $ficheros
     * @return array<int, int>
     */
    private static function importados(ScssCdnBundle $bundle, $ficheros): array
    {
        $importados = [];

        foreach ($ficheros as $fichero) {
            $contenido = ScssCdnStorageService::getFile(
                $bundle->product,
                $bundle,
                $fichero->relative_path
            );

            if ($contenido === null) {
                continue;
            }

            foreach (ScssCdnImportParser::parseImports($contenido) as $import) {
                // `parseImports` devuelve la ruta escrita en el `@import`; resolverla a un
                // fichero del bundle es lo que hace el parser al servir, y es lo que hay que
                // usar aquí: si aquí se resolviera de otra forma, la detección diría una
                // cosa y el servicio haría otra.
                $ruta = is_array($import) ? ($import['path'] ?? null) : $import;

                if (! is_string($ruta) || $ruta === '') {
                    continue;
                }

                $destino = ScssCdnImportParser::findFileInBundle(
                    $bundle,
                    $ruta,
                    $fichero->relative_path
                );

                if ($destino !== null) {
                    $importados[] = $destino->id;
                }
            }
        }

        return array_values(array_unique($importados));
    }
}
