<?php

namespace App\Support;

use App\Models\Products\Product;
use Illuminate\Database\Eloquent\Model;

/**
 * Qué pasa si se borra una versión de contenido (PRODSECU-198).
 *
 * **Por qué esto no es «poner un botón de borrar».** Una versión de contenido no es un
 * registro del panel: **se está sirviendo a los Moodles de los clientes**. Quien decide qué
 * versión le toca a cada sitio es `ResuelveVersionCompatible::findCompatibleVersion()`, que
 * devuelve la más alta menor o igual a la que pide el plugin. Así que borrar una versión
 * **cambia qué recibe un cliente en su siguiente petición**, sin que él haya tocado nada y
 * sin que nadie se entere.
 *
 * Y hay un caso peor que el obvio: si se borra la única versión, el producto deja de servir
 * ese contenido y la API empieza a responder «sin contenido», que
 * [`norma-cero-errores.md`](../../.tresipunt/tresipunt/manager/norma-cero-errores.md)
 * clasifica como **respuesta correcta**. O sea que **no salta ninguna alarma**: ni el
 * Dashboard, ni el visor, ni los avisos. El sitio se queda sin contenido en silencio.
 *
 * De ahí que la modal de borrado no pregunte «¿seguro?», sino que **enseñe a qué versión
 * va a caer cada entorno**. Ese es el dato con el que se decide.
 *
 * **Lo que se calcula y lo que no.** Se distingue a propósito:
 *
 * - **Quién la recibe ahora** se calcula, y es exacto: sale de la misma resolución que usa
 *   la API sobre el inventario de plugins de cada entorno.
 * - **Quién la ha pedido** sale del log de peticiones, que tiene retención: «nadie la ha
 *   pedido» puede querer decir «no en la ventana que se conserva». Se dice con esas
 *   palabras.
 */
class ImpactoDeBorrarVersion
{
    /**
     * @param  Model  $version  la versión que se quiere borrar
     * @param  class-string<Model>  $modelo  su clase, para buscar las hermanas
     * @return array{
     *     esLaUnica: bool,
     *     quedan: int,
     *     noQuedaraNingunaServida: bool,
     *     quedanServidas: int,
     *     afectados: array<int, array{entorno:mixed, instalada:string, pasaraA:?string}>,
     *     seQuedanSinNada: int,
     *     elementos: int,
     *     publicados: int
     * }
     */
    public static function de(Product $producto, Model $version, string $modelo, int $elementos = 0, int $publicados = 0): array
    {
        $filas = $modelo::where('product_id', $producto->id)
            ->get(['version', 'status']);

        $estaVersion = (string) $version->version;

        // **Solo las que se sirven.** El cálculo tiene que dar lo mismo que da la API, y la
        // API solo sirve las publicadas: desde que hay borradores y retiradas (MGR-023),
        // contar todas las filas daba un número falso —una versión en borrador más alta que
        // la publicada se llevaba el «la reciben 3»— y un «pasará a» que apuntaba a una
        // versión que nadie va a recibir.
        $todas = $filas
            ->filter(fn ($fila) => $fila->status === EstadoDePublicacion::PUBLICADA)
            ->map(fn ($fila) => (string) $fila->version)
            ->filter()
            ->values();

        // Las que quedarían servibles. **Es el cálculo que importa**: sobre ellas se
        // resuelve a qué caería cada entorno, con el mismo algoritmo que la API.
        $sinEsta = $todas->reject(fn (string $v) => $v === $estaVersion)->values();

        // Y las filas que quedarían en la pantalla, que es otra cosa: la modal de borrado
        // dice si la sección se queda sin ninguna, y ahí cuentan también los borradores.
        $filasSinEsta = $filas
            ->map(fn ($fila) => (string) $fila->version)
            ->filter()
            ->reject(fn (string $v) => $v === $estaVersion)
            ->values();

        $afectados = [];
        $seQuedanSinNada = 0;

        foreach (RepartoDeVersiones::entornosDe($producto) as $cliente) {
            $instalada = $cliente['instalada'];

            // Sin el plugin instalado no recibe nada de este producto, así que borrar no le
            // cambia nada. No se cuenta como afectado ni se pinta: llenaría la lista de
            // entornos que no tienen que ver con esta decisión.
            if ($instalada === null || $instalada === '') {
                continue;
            }

            // ¿Le está llegando **esta** versión? Si no, borrarla no le afecta.
            if (RepartoDeVersiones::queRecibe($todas, (string) $instalada) !== $estaVersion) {
                continue;
            }

            $pasaraA = RepartoDeVersiones::queRecibe($sinEsta, (string) $instalada);

            if ($pasaraA === null) {
                $seQuedanSinNada++;
            }

            $afectados[] = [
                'entorno' => $cliente['entorno'],
                'instalada' => (string) $instalada,
                // `null` aquí es lo grave: **ninguna versión le sirve** después de borrar,
                // así que ese sitio deja de recibir este contenido.
                'pasaraA' => $pasaraA,
            ];
        }

        // Los que se quedan sin nada primero: es lo que hay que mirar antes de confirmar.
        usort($afectados, function (array $a, array $b) {
            return [$a['pasaraA'] === null ? 0 : 1, $a['entorno']->name]
                <=> [$b['pasaraA'] === null ? 0 : 1, $b['entorno']->name];
        });

        // **La regla, y es lo que decide el botón.** Solo se puede borrar una versión que
        // no le esté llegando a nadie y que esté vacía. No es una precaución de más: es que
        // las dos cosas tienen arreglo previo —vaciarla, o esperar a que nadie la reciba— y
        // el borrado no. Además convierte una decisión con consecuencias en clientes en una
        // operación sin consecuencias, que es lo que se puede ofrecer con un botón.
        $motivos = [];

        if ($afectados !== []) {
            $motivos[] = count($afectados) === 1
                ? 'un entorno la está recibiendo ahora mismo'
                : count($afectados) . ' entornos la están recibiendo ahora mismo';
        }

        if ($elementos > 0) {
            $motivos[] = $elementos === 1
                ? 'todavía tiene un elemento dentro'
                : 'todavía tiene ' . $elementos . ' elementos dentro';
        }

        return [
            // `esLaUnica` y `quedan` cuentan **filas**: es lo que la modal de borrado usa
            // para decir si la sección se queda sin ninguna versión. `…Servidas` cuenta las
            // que la API sirve, que es lo que decide si un cliente se queda sin contenido.
            'esLaUnica' => $filasSinEsta->isEmpty(),
            'quedan' => $filasSinEsta->count(),
            'noQuedaraNingunaServida' => $sinEsta->isEmpty(),
            'quedanServidas' => $sinEsta->count(),
            'afectados' => $afectados,
            'seQuedanSinNada' => $seQuedanSinNada,
            'elementos' => $elementos,
            'publicados' => $publicados,
            // Las dos condiciones, y por separado: la vista tiene que poder decir cuál
            // falla, porque se arreglan de formas distintas.
            'sePuedeBorrar' => $afectados === [] && $elementos === 0,
            'laUsaAlguien' => $afectados !== [],
            'tieneElementos' => $elementos > 0,
            'motivos' => $motivos,
        ];
    }
}
