<?php

namespace App\Support;

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

/**
 * Qué versión de contenido le toca a un Moodle: **la regla de negocio central del Manager**.
 *
 * La API no sirve la última versión publicada: sirve **la más alta que no supere la
 * versión del plugin instalado en ese sitio**. De ahí que publicar una versión más alta
 * que el plugin de todos no le llegue a nadie, y que publicar una intermedia mueva a los
 * que recibían la anterior. Es el algoritmo del que depende qué recibe cada cliente.
 *
 * **Estaba escrito siete veces** —`FeatureVersion`, `TutorialVersion`, `ResourceVersion`,
 * `ScssVersion`, `JsVersion`, `ScssCdnBundle` y `Setup`—, con el mismo cuerpo palabra por
 * palabra y hasta el mismo número de línea en seis de ellas. Definida siete veces,
 * cualquier matiz —¿qué pasa con versiones iguales?, ¿se respeta el estado?, ¿y si el
 * número no es numérico?— hay que aplicarlo siete veces, y una divergencia hace que un
 * tipo de contenido se comporte distinto que los demás **sin que nada lo avise**. Ver
 * MGR-024, y MGR-023, que es exactamente eso: la única que filtraba por estado era
 * `Setup`.
 *
 * ## Y ahora resuelve en SQL
 *
 * Las siete copias hacían `forProduct($id)->get()` —**todas** las versiones del producto a
 * memoria— y después filtraban y ordenaban con `filter()` y `sortByDesc()`. Con tres
 * versiones da igual; crece con cada publicación y se paga **en cada petición de cada
 * entorno**. Aquí son dos consultas acotadas, y la segunda se resuelve con `LIMIT 1`.
 *
 * ## `version + 0` y no `CAST`
 *
 * `version` es una columna de texto con un número de diez dígitos (`YYYYMMDDXX`), así que
 * comparar contra un entero **hay que forzarlo**: en sqlite un entero y un texto no se
 * comparan por valor —los enteros van antes que cualquier texto— y `where('version','<=',
 * 2026010100)` devolvería cualquier cosa. Y `CAST(... AS INTEGER)` vale en sqlite pero no
 * en MySQL, que quiere `SIGNED`.
 *
 * `version + 0` funciona en los tres motores que toca este proyecto —MariaDB en
 * producción, MySQL en local y sqlite en los tests—, que es el mismo criterio por el que
 * los percentiles del visor usan OFFSET y no `PERCENTILE_CONT`.
 */
trait ResuelveVersionCompatible
{
    /**
     * La versión que le corresponde a un plugin en la versión que dice tener.
     *
     * @param  Product|int  $product
     */
    public static function findCompatibleVersion($product, string $productVersion): ?static
    {
        $productId = $product instanceof Product ? $product->id : $product;

        // **La coincidencia exacta va primero y por igualdad de texto.** Es cómo estaba
        // escrito en las siete copias y se conserva a propósito: el orden numérico ya
        // devolvería la exacta cuando existe, pero con un valor que no sea un número
        // —`version + 0` lo convierte en 0— la igualdad de texto sigue acertando y el
        // orden numérico no. Cuesta una consulta por índice.
        $exacta = static::consultaDeVersionesServibles($productId)
            ->where('version', $productVersion)
            ->first();

        if ($exacta !== null) {
            return $exacta;
        }

        return static::consultaDeVersionesServibles($productId)
            ->whereRaw('version + 0 <= ?', [(int) $productVersion])
            ->orderByRaw('version + 0 desc')
            ->first();
    }

    /**
     * De qué versiones se puede tirar para este producto: **las publicadas**.
     *
     * Esto era un punto de extensión, y ha dejado de hacer falta. Existía porque `Setup` era
     * el único tipo de contenido con columna de estado (MGR-023) y lo sobrescribía para
     * añadir su `active()`; los otros seis no tenían nada que filtrar, y el docblock decía
     * «cuando lo tengan, se filtra en un solo sitio». **Ese día es este**: los siete tienen
     * estado, así que el filtro vive aquí y una sola vez.
     *
     * Un borrador no se sirve aunque su número cuadre, y una retirada tampoco: contarlas
     * como servibles es lo contrario de lo que significan. Los estados los define
     * {@see TieneEstadoDePublicacion}, que es de donde sale el `active()`.
     */
    protected static function consultaDeVersionesServibles(int $productId): Builder
    {
        return static::forProduct($productId)->active();
    }
}
