<?php

namespace App\Support;

use App\Models\Environments\Environment;
use App\Models\Environments\Plugin;
use App\Models\Products\Product;
use Illuminate\Support\Collection;

/**
 * Qué versión de contenido está recibiendo cada cliente de un producto.
 *
 * **Es lo que los siete listados de contenido versionado no decían.** Enseñaban «versión
 * 2026080702 · 1 archivo · creada por Antonio · 27/08/2026» y con eso no se puede
 * responder la única pregunta que importa después de publicar: **¿le ha llegado a
 * alguien?**
 *
 * La API no sirve «la última versión»: sirve **la versión más alta que sea menor o igual
 * a la del plugin instalado** en ese sitio. De ahí salen tres situaciones que el listado
 * pintaba exactamente igual:
 *
 * - **Una versión que nadie recibe.** Se publica contenido con una versión más alta que la
 *   del plugin de todos los clientes y no le llega a nadie: para recibirla, cada cliente
 *   tiene que actualizar su plugin. No hay operación inversa —«dame lo más nuevo»—, así
 *   que el contenido se queda ahí sin que nada falle ni avise.
 * - **Una versión que tapa a la anterior.** Al publicar una versión intermedia, los
 *   clientes que estaban recibiendo la vieja pasan a la nueva sin que nadie lo pida.
 * - **Un cliente que recibe un error.** Si su plugin es más antiguo que la versión de
 *   contenido más antigua que existe, no hay ninguna compatible y la API le devuelve
 *   `3002` / `4002` / `5002`. Desde el panel eso no se veía.
 *
 * Y una cuarta que no es de versiones: **un entorno con licencia que no tiene el plugin
 * instalado**. No pide contenido, así que no recibe nada y tampoco da error. En el
 * producto del SEPE son cuatro de cinco.
 */
class RepartoDeVersiones
{
    /**
     * Los clientes de un producto y la versión del plugin que tienen puesta.
     *
     * La versión sale del **inventario de plugins** del entorno, cruzando el `slug` del
     * producto con el `component` de Moodle: son el mismo identificador, y por eso este
     * cruce no necesita ninguna columna nueva.
     *
     * Se usa `versiondb` y no `versiondisk`: lo que el plugin envía a la API es la versión
     * que su base de datos tiene migrada, que es la que de verdad está corriendo.
     *
     * @return Collection<int, array{entorno:Environment, instalada:?string}>
     */
    public static function entornosDe(Product $producto): Collection
    {
        $entornos = $producto->environmentsQuery()
            ->where('active', true)
            ->with('client:id,name')
            ->get(['environments.id', 'environments.name', 'environments.domain', 'environments.env', 'environments.client_id']);

        if ($entornos->isEmpty()) {
            return collect();
        }

        // Una consulta para todos: el inventario de los entornos que nos interesan.
        $instaladas = Plugin::query()
            ->whereIn('environment_id', $entornos->pluck('id'))
            ->where('component', $producto->slug)
            ->pluck('versiondb', 'environment_id');

        return $entornos->map(fn (Environment $entorno) => [
            'entorno' => $entorno,
            // Null significa **dos cosas distintas** y hay que poder distinguirlas: o el
            // sitio no envía inventario, o lo envía y este plugin no está. Quien consuma
            // esto lo resuelve con la cobertura del inventario; aquí solo se dice que no
            // consta.
            'instalada' => $instaladas[$entorno->id] ?? null,
        ]);
    }

    /**
     * Para cada versión publicada, quién la está recibiendo.
     *
     * @param  Collection<int, string>|array<int, string>  $versiones  las versiones publicadas
     * @return array{
     *     porVersion: array<string, int>,
     *     sinPlugin: int,
     *     conError: int,
     *     clientes: int
     * }
     */
    public static function reparto(Product $producto, Collection|array $versiones): array
    {
        $publicadas = collect($versiones)
            ->map(fn ($v) => (string) $v)
            ->filter()
            // Descendente: la resolución busca «la más alta ≤ la del plugin», así que la
            // primera que cumple es la respuesta. Como cadenas, porque la marca
            // `YYYYMMDDXX` desborda un entero de 32 bits.
            ->sortDesc()
            ->values();

        $porVersion = $publicadas->mapWithKeys(fn (string $v) => [$v => 0])->all();
        $sinPlugin = 0;
        $conError = 0;
        $clientes = self::entornosDe($producto);

        foreach ($clientes as $cliente) {
            $instalada = $cliente['instalada'];

            if ($instalada === null || $instalada === '') {
                $sinPlugin++;

                continue;
            }

            $recibe = $publicadas->first(fn (string $v) => $v <= (string) $instalada);

            if ($recibe === null) {
                // Su plugin es más antiguo que la versión de contenido más antigua: la API
                // le devuelve un error, no un contenido viejo.
                $conError++;

                continue;
            }

            $porVersion[$recibe]++;
        }

        return [
            'porVersion' => $porVersion,
            'sinPlugin' => $sinPlugin,
            'conError' => $conError,
            'entornos' => $clientes->count(),
        ];
    }

    /**
     * Una fila por cliente: qué plugin tiene, qué le corresponde y qué ha pedido.
     *
     * **Es la vista que pedía el diseño** y la que responde de un golpe las cuatro
     * situaciones que antes estaban repartidas en dos bloques distintos: el que lo recibe,
     * el que tiene el plugin y **no llama nunca**, el que llama y se lleva un error, y el
     * que tiene licencia y no ha instalado nada.
     *
     * Los que no tienen el plugin **se agrupan en una fila**. Son 19 de 21 en el catálogo
     * real, y una fila por cada uno llenaría la tabla de entornos que no piden nada: no es
     * un problema de esta pantalla —aquí no se instala el plugin de nadie— sino una
     * conversación comercial, así que se dice en una línea y con esas palabras.
     *
     * @param  Collection<int, string>|array<int, string>  $versiones  las que se sirven
     * @param  array<string, mixed>  $peticiones  lo que devuelve `PeticionesDeContenido::de()`
     * @return array<int, array<string, mixed>>
     */
    public static function porEntorno(
        Product $producto,
        Collection|array $versiones,
        array $peticiones = [],
        array $idsPorVersion = []
    ): array {
        $publicadas = collect($versiones)->map(fn ($v) => (string) $v)->filter()->sortDesc()->values();
        $clientes = self::entornosDe($producto);

        // Lo que ha pedido cada entorno, indexado para cruzarlo sin recorrer la lista por
        // cada cliente.
        $pedido = collect($peticiones['porEntorno'] ?? [])
            ->filter(fn (array $p) => $p['entorno'] !== null)
            ->keyBy(fn (array $p) => $p['entorno']->id);

        $filas = [];
        $sinPlugin = 0;

        foreach ($clientes as $cliente) {
            $instalada = $cliente['instalada'];

            if ($instalada === null || $instalada === '') {
                $sinPlugin++;

                continue;
            }

            $suyo = $pedido->get($cliente['entorno']->id);

            $recibe = self::queRecibe($publicadas, $instalada);

            $filas[] = [
                'entorno' => $cliente['entorno'],
                'instalada' => (string) $instalada,
                // Qué le corresponde según la resolución de la API.
                'recibe' => $recibe,
                // Y su id, para poder enlazar desde la tabla a la versión que ese entorno
                // está recibiendo: era el paso que faltaba —se veía el número y había que
                // buscarlo a mano en el listado—.
                'recibeId' => $recibe !== null ? ($idsPorVersion[$recibe] ?? null) : null,
                // Y qué ha pedido de verdad. `null` aquí significa **no ha llamado**, que
                // no es lo mismo que «no le corresponde nada»: el plugin está instalado y
                // su tarea programada no está llamando.
                'peticiones' => $suyo['peticiones'] ?? 0,
                'ultima' => $suyo['ultima'] ?? null,
                'fallidas' => $suyo['fallidas'] ?? 0,
                'siempreFalla' => $suyo['siempreFalla'] ?? false,
                'ultimoError' => $suyo['ultimoError'] ?? null,
            ];
        }

        // Los que reciben algo primero, y dentro de ellos los que tienen algún problema:
        // el orden lleva arriba lo que hay que mirar.
        usort($filas, function (array $a, array $b) {
            $problema = fn (array $f) => ($f['recibe'] === null || $f['siempreFalla'] || $f['peticiones'] === 0) ? 0 : 1;

            return [$problema($a), $a['entorno']->name] <=> [$problema($b), $b['entorno']->name];
        });

        if ($sinPlugin > 0) {
            // La fila agrupada, siempre al final: es contexto, no trabajo de esta pantalla.
            $filas[] = [
                'entorno' => null,
                'agrupados' => $sinPlugin,
                'instalada' => null,
                'recibe' => null,
                'recibeId' => null,
                'peticiones' => 0,
                'ultima' => null,
                'fallidas' => 0,
                'siempreFalla' => false,
                'ultimoError' => null,
            ];
        }

        return $filas;
    }

    /**
     * La versión que recibiría un plugin en la versión dada, o null si ninguna sirve.
     *
     * El mismo algoritmo que las Actions de la API, en un solo sitio: si se copia en cada
     * pantalla, el día que cambie la resolución el panel dirá una cosa y la API hará otra.
     *
     * @param  Collection<int, string>|array<int, string>  $versiones
     */
    public static function queRecibe(Collection|array $versiones, ?string $instalada): ?string
    {
        if ($instalada === null || $instalada === '') {
            return null;
        }

        return collect($versiones)
            ->map(fn ($v) => (string) $v)
            ->filter()
            ->sortDesc()
            ->first(fn (string $v) => $v <= $instalada);
    }
}
