<?php

namespace App\Livewire\Products;

use App\Models\Products\Product;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;

/**
 * Listado de productos.
 *
 * Implementa `Productos.dc.html` del sistema de diseño (Claude Design).
 *
 * **Lo que había**: un buscador y una rejilla de tarjetas con la imagen, el nombre y el
 * resumen. Nada decía **qué tiene publicado** cada producto, que es la pregunta de
 * Desarrollo y de Contenido, ni distinguía los dos estados —operativo y a la venta— que
 * el modelo lleva separados y la pantalla juntaba en un «Activo».
 *
 * Con 5 productos —y unas decenas como techo— esto **no necesita densidad**: necesita
 * decir el estado de cada uno de un vistazo. De ahí la rejilla de dos columnas con los
 * contadores dentro de la tarjeta.
 */
class Index extends Component
{
    public string $search = '';

    /** Familia del plugin: theme, block, report, local… vacío = todas. */
    public string $tipo = '';

    protected $queryString = [
        'search',
        'tipo' => ['except' => ''],
    ];

    /**
     * Los siete cajones de contenido versionado, en orden y agrupados por familia.
     *
     * **Está aquí y no en la vista** porque lo usan el listado y la ficha, y porque cada
     * cajón necesita cuatro cosas que hay que mantener juntas: su modelo, su nombre, su
     * ruta y su permiso. Repartido, se desincroniza.
     *
     * @return array<string, array{nombre:string, modelo:class-string, ruta:string, permiso:string, familia:string}>
     */
    public static function cajones(): array
    {
        return [
            'setups' => [
                'nombre' => 'Setups',
                'modelo' => \App\Models\Setups\Setup::class,
                'ruta' => 'products.setups.index',
                'permiso' => 'admin.setups.index',
                'familia' => 'config',
                // **Solo los activos.** `Setup` es el único cajón con `status`, y la
                // API sirve únicamente los `active` (`findCompatibleVersion()`):
                // contar un setup deprecado haría que la pantalla dijera «publicado»
                // de algo que ningún Moodle recibe. Los otros seis no tienen estado,
                // así que todas sus filas cuentan.
                'filtro' => fn ($query) => $query->where('status', 'active'),
            ],
            'scss' => [
                'nombre' => 'SCSS',
                'modelo' => \App\Models\Scss\ScssVersion::class,
                'ruta' => 'products.scss.index',
                'permiso' => 'admin.scss.index',
                'familia' => 'config',
            ],
            'js' => [
                'nombre' => 'JS',
                'modelo' => \App\Models\Js\JsVersion::class,
                'ruta' => 'products.js.index',
                'permiso' => 'admin.js.index',
                'familia' => 'config',
            ],
            'cdn' => [
                'nombre' => 'Bundles SCSS CDN',
                'modelo' => \App\Models\ScssCdn\ScssCdnBundle::class,
                'ruta' => 'products.scss-cdn.index',
                'permiso' => 'admin.scss-cdn.index',
                'familia' => 'config',
            ],
            'features' => [
                'nombre' => 'Features',
                'modelo' => \App\Models\Features\FeatureVersion::class,
                'ruta' => 'products.features.index',
                'permiso' => 'admin.features.index',
                'familia' => 'contenido',
            ],
            'resources' => [
                'nombre' => 'Recursos',
                'modelo' => \App\Models\Resources\ResourceVersion::class,
                'ruta' => 'products.resources.index',
                'permiso' => 'admin.resources.index',
                'familia' => 'contenido',
            ],
            'tutorials' => [
                'nombre' => 'Tutoriales',
                'modelo' => \App\Models\Tutorials\TutorialVersion::class,
                'ruta' => 'products.tutorials.index',
                'permiso' => 'admin.tutorials.index',
                'familia' => 'contenido',
            ],
        ];
    }

    /**
     * Qué tiene publicado cada producto, por cajón.
     *
     * **Una consulta agregada por cajón para todos los productos**, no siete por
     * producto: son siete tablas distintas y con 40 productos serían 280 consultas.
     * Cacheado 60 segundos porque `render()` se ejecuta en cada tecla del buscador.
     *
     * De cada cajón interesan tres cosas: **cuántas versiones hay**, **cuál es la
     * última** y **de cuándo es**. Publicar es la acción de esta pantalla, así que la
     * fecha del último cambio es el dato que más se mira.
     *
     * @return array<int, array<string, array{n:int, version:string, fecha:?\Illuminate\Support\Carbon}>>
     */
    public static function publicado(): array
    {
        return Cache::remember('products:publicado', 60, function () {
            $porProducto = [];

            foreach (self::cajones() as $clave => $cajon) {
                $filtro = $cajon['filtro'] ?? null;

                // **Con `if` y no con `when()`.** `when()` resuelve un Closure pasado
                // como condición —lo invoca para obtener el valor— y acababa aplicando
                // el filtro dos veces.
                $consulta = $cajon['modelo']::query()
                    ->selectRaw('product_id, count(*) as n, max(version) as ultima')
                    ->whereNotNull('product_id');

                if ($filtro !== null) {
                    $filtro($consulta);
                }

                $filas = $consulta->groupBy('product_id')->get();

                if ($filas->isEmpty()) {
                    continue;
                }

                // ============ Las fechas, de golpe ============
                // La fecha de la última versión se pide aparte porque `max(created_at)`
                // no tiene por qué ser la fecha de la versión más alta: se puede publicar
                // una versión antigua después de una nueva.
                //
                // Pero **una consulta por producto era el N+1 de MGR-016 otra vez**: con
                // 7 cajones y 40 productos, hasta 280 consultas en el fallo de caché. Se
                // piden todas juntas y se emparejan en PHP; el producto cartesiano del
                // `whereIn` no importa porque solo se leen los pares que interesan.
                $deLasFechas = $cajon['modelo']::query()
                    ->select('product_id', 'version', 'created_at')
                    ->whereIn('product_id', $filas->pluck('product_id'))
                    ->whereIn('version', $filas->pluck('ultima'))
                    // Ascendente y quedándose con la primera: es lo que hacía
                    // `value('created_at')` cuando hay varias filas del mismo par.
                    ->orderBy('id');

                if ($filtro !== null) {
                    // El filtro también aquí: en «setups» descarta las deprecadas, y sin
                    // esto la fecha podría venir de una fila que el recuento no cuenta.
                    $filtro($deLasFechas);
                }

                $fechas = [];

                foreach ($deLasFechas->get() as $fila) {
                    $fechas[$fila->product_id . '|' . $fila->version] ??= $fila->created_at;
                }

                foreach ($filas as $fila) {
                    $fecha = $fechas[$fila->product_id . '|' . $fila->ultima] ?? null;

                    $porProducto[$fila->product_id][$clave] = [
                        'n' => (int) $fila->n,
                        'version' => (string) $fila->ultima,
                        'fecha' => $fecha ? \Illuminate\Support\Carbon::parse($fecha) : null,
                    ];
                }
            }

            return $porProducto;
        });
    }

    /**
     * La última publicación de un producto, sea de qué cajón sea.
     *
     * Es lo que va en el pie de la tarjeta: responde «¿este producto está vivo?» mejor
     * que cualquier contador.
     *
     * @param  array<string, array<string, mixed>>  $cajonesDelProducto
     * @return array{cajon:string, version:string, fecha:?\Illuminate\Support\Carbon}|null
     */
    public static function ultimaPublicacion(array $cajonesDelProducto): ?array
    {
        $mejor = null;
        $nombres = self::cajones();

        foreach ($cajonesDelProducto as $clave => $datos) {
            if ($mejor === null || $datos['version'] > $mejor['version']) {
                $mejor = [
                    'cajon' => $nombres[$clave]['nombre'],
                    'version' => $datos['version'],
                    'fecha' => $datos['fecha'],
                ];
            }
        }

        return $mejor;
    }

    /**
     * Abre la modal de borrado.
     *
     * **Antes había un `delete()` que borraba directamente, y ninguna pantalla lo
     * llamaba**: ni esta ni su gemela de Configuración pintaban un botón, así que no
     * se podía borrar un producto desde el panel. Tercera vez que aparece el patrón
     * de código sin punto de entrada (MGR-039, MGR-045).
     *
     * La modal, además, **bloquea el borrado de un producto que alguien tenga
     * contratado**: borrarlo no da un error, hace que el cliente vea la promoción de
     * algo que ya ha pagado. La explicación está en `ProductDeleteModal`.
     */
    public function abrirBorrado(int $productId): void
    {
        $this->authorize('admin.products.destroy');

        $this->dispatch('openModal',
            component: \App\Livewire\Products\ProductDeleteModal::class,
            arguments: ['productId' => $productId]
        );
    }

    #[\Livewire\Attributes\On('productsChanged')]
    public function refrescar(): void
    {
        // El recuento de publicado va cacheado 60 s y el producto ya no está: se
        // olvida para que la rejilla no siga contando lo que se acaba de borrar.
        Cache::forget('products:publicado');
    }

    public function limpiarFiltros(): void
    {
        $this->reset(['search', 'tipo']);
    }

    public function render()
    {
        $publicado = self::publicado();

        $products = Product::query()
            ->withCount('tokens')
            // Los entornos, en la misma consulta: antes era un `count()` por tarjeta
            // (MGR-016).
            ->withEnvironmentsCount()
            ->when($this->search !== '', function ($query) {
                // Los `orWhere` en su propio paréntesis: con el filtro de tipo puesto,
                // sueltos harían que la búsqueda se lo saltara (MGR-054).
                $query->where(function ($sub) {
                    $termino = '%' . $this->search . '%';

                    $sub->where('name', 'like', $termino)
                        ->orWhere('slug', 'like', $termino);
                });
            })
            ->when($this->tipo !== '', fn ($query) => $query->where('type', $this->tipo))
            // Los que no sirven contenido, al final: se ven, pero no estorban.
            ->orderByDesc('actived')
            ->orderBy('name')
            ->get();

        // Los tipos que existen de verdad, para no ofrecer filtros vacíos.
        $tipos = Product::query()
            ->whereNotNull('type')
            ->distinct()
            ->orderBy('type')
            ->pluck('type');

        $total = Product::count();
        $conContenido = collect($publicado)->keys()->count();

        return view('livewire.products.index', [
            'products' => $products,
            'publicado' => $publicado,
            'tipos' => $tipos,
            'resumen' => [
                'total' => $total,
                'conContenido' => $conContenido,
                'sinContenido' => $total - $conContenido,
            ],
        ])->layout('layouts.app');
    }
}
