<?php

namespace App\Livewire\Configurations;

use App\Models\Environments\Plugin;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Products\OwnPlugin;
use App\Models\Products\Product;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;

/**
 * Catálogo de plugins propios — Configuración → Catálogos.
 *
 * **Qué resuelve.** El Manager tenía que saber qué plugins de un Moodle son nuestros, y lo
 * adivinaba buscando «tresipunt», «fresk», «fundae» o «3ip» en el identificador. Eso
 * fallaba en las dos direcciones —`local_courseai` es nuestro y no lleva ninguna de esas
 * palabras; un plugin de terceros con «fundae» en el nombre se colaba— y la lista vivía en
 * el código, así que añadir uno obligaba a desplegar.
 *
 * **Por qué no vale el catálogo de Productos para esto.** Un producto tiene ficha,
 * licencias y contenido versionado, y su alta es un formulario largo. La mayoría de
 * nuestros plugins no son productos comerciales: `format_fresk` o `local_tresipuntimportgc`
 * no los licencia nadie por separado. Aquí solo se declara el identificador, que es lo
 * único que hacía falta.
 *
 * Los dos catálogos identifican: un plugin es nuestro si está aquí **o** si es un
 * producto. Ver `OwnPlugin::sonNuestros()`.
 *
 * **Y se puede dar de alta desde lo que ya hay instalado**: la pantalla ofrece los
 * `component` que aparecen en el inventario de algún entorno y aún no están declarados,
 * para no tener que escribirlos a mano —que es donde se cometen los dedazos, y un
 * identificador mal escrito no empareja con nada—.
 */
class OwnPlugins extends Component
{
    /** El identificador que se está añadiendo. */
    public string $component = '';

    public string $name = '';

    public string $notes = '';

    /**
     * Cuántas sugerencias se ofrecen.
     *
     * Son los `component` del inventario que aún no están declarados: en un Moodle hay 464
     * plugins y casi ninguno es nuestro, así que se ordenan por parecido con nuestras
     * marcas y se cortan. Escribir el identificador a mano sigue estando disponible.
     */
    private const CUANTAS_SUGERENCIAS = 12;

    /**
     * Pistas de nombre, **solo para ordenar las sugerencias**.
     *
     * No identifican nada —eso lo hace este catálogo— pero sirven para poner arriba lo que
     * probablemente haya que dar de alta, en vez de una lista alfabética de 464.
     */
    private const PISTAS = ['tresipunt', 'fresk', 'fundae', '3ip'];

    protected function rules(): array
    {
        return [
            // El formato de un `component` de Moodle: `tipo_nombre`, minúsculas y
            // guiones bajos. Se valida porque un identificador mal escrito no empareja
            // con nada y el error es silencioso: el plugin simplemente no sale como
            // nuestro.
            'component' => [
                'required',
                'string',
                'max:191',
                'regex:/^[a-z][a-z0-9]*_[a-z0-9_]+$/',
                'unique:own_plugins,component',
            ],
            'name' => ['nullable', 'string', 'max:255'],
            'notes' => ['nullable', 'string', 'max:500'],
        ];
    }

    protected function messages(): array
    {
        return [
            'component.regex' => 'Un identificador de Moodle tiene la forma `tipo_nombre`, '
                . 'en minúsculas: local_tresipunt, theme_fresk, mod_customcert.',
            'component.unique' => 'Ese plugin ya está en el catálogo.',
        ];
    }

    public function anadir(): void
    {
        $this->authorize('admin.configs.edit');

        $datos = $this->validate();

        $plugin = OwnPlugin::create([
            'component' => $datos['component'],
            // Si no se pone nombre, se coge el que venga del inventario: es el que el
            // propio plugin declara y es mejor que dejarlo vacío.
            'name' => $datos['name'] !== ''
                ? $datos['name']
                : Plugin::where('component', $datos['component'])->value('name'),
            'notes' => $datos['notes'] !== '' ? $datos['notes'] : null,
        ]);

        MonitoringLog::db(
            'info',
            '16028',
            'Plugin declarado como propio: ' . $plugin->component
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'OwnPlugin',
            (string) $plugin->id
        );

        $this->reset(['component', 'name', 'notes']);

        session()->flash('success', '«' . $plugin->component . '» ya cuenta como plugin nuestro. '
            . 'Aparecerá como tal en el inventario de todos los entornos que lo tengan instalado.');
    }

    /** Añade uno de los sugeridos, sin escribir nada. */
    public function anadirSugerido(string $component): void
    {
        $this->authorize('admin.configs.edit');

        $this->component = $component;
        $this->name = (string) Plugin::where('component', $component)->value('name');
        $this->notes = '';

        $this->anadir();
    }

    public function quitar(int $id): void
    {
        $this->authorize('admin.configs.edit');

        $plugin = OwnPlugin::findOrFail($id);
        $component = $plugin->component;

        // Lógico: quitar uno de la lista no debería perder quién lo puso ni cuándo, y
        // volver a añadirlo es un clic.
        $plugin->delete();

        MonitoringLog::db(
            'info',
            '16029',
            'Plugin retirado del catálogo de propios: ' . $component
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'OwnPlugin',
            (string) $id
        );

        session()->flash('success', '«' . $component . '» ya no cuenta como plugin nuestro.');
    }

    /**
     * Los declarados, con lo que se sabe de cada uno.
     *
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function declarados()
    {
        // Las instalaciones, de una consulta agregada: preguntarlo por fila era el N+1 de
        // siempre.
        $instalaciones = Plugin::query()
            ->selectRaw('component, count(distinct environment_id) as entornos')
            ->groupBy('component')
            ->pluck('entornos', 'component');

        $productos = Product::query()->pluck('name', 'slug');

        return OwnPlugin::orderBy('component')
            ->get()
            ->map(fn (OwnPlugin $plugin) => [
                'id' => $plugin->id,
                'component' => $plugin->component,
                'name' => $plugin->name,
                'notes' => $plugin->notes,
                'entornos' => (int) ($instalaciones[$plugin->component] ?? 0),
                // Si además es un producto, se dice: significa que tiene ficha, licencias
                // y contenido detrás.
                'producto' => $productos[$plugin->component] ?? null,
            ]);
    }

    /**
     * Los productos del catálogo, que **también son nuestros** sin estar en esta lista.
     *
     * Se enseñan para que nadie los añada dos veces ni piense que faltan.
     *
     * @return \Illuminate\Support\Collection<int, string>
     */
    public function porSerProducto()
    {
        $declarados = OwnPlugin::pluck('component')->all();

        return Product::orderBy('slug')
            ->get()
            // Mismo motivo que en «licenciado y sin instalar»: esta lista dice qué
            // componentes son nuestros, y un ecosistema comercial no es un componente.
            // Ofrecerlo aquí invita a darlo de alta como plugin propio.
            ->filter(fn (Product $producto) => $producto->esPluginDeMoodle())
            ->reject(fn (Product $producto) => in_array($producto->slug, $declarados, true))
            ->map(fn (Product $producto) => [
                'component' => $producto->slug,
                'name' => $producto->name,
            ])
            ->values();
    }

    /**
     * Qué se puede dar de alta de lo que ya está instalado en algún entorno.
     *
     * Ordenado por parecido con nuestras marcas: en un Moodle hay 464 plugins y casi
     * ninguno es nuestro, así que una lista alfabética no serviría de nada.
     *
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function sugerencias()
    {
        $yaNuestros = OwnPlugin::sonNuestros();

        return Plugin::query()
            ->selectRaw('component, max(name) as name, count(distinct environment_id) as entornos')
            ->whereNotIn('component', $yaNuestros)
            ->groupBy('component')
            ->get()
            ->map(function ($fila) {
                $component = mb_strtolower((string) $fila->component);
                $suena = false;

                foreach (self::PISTAS as $pista) {
                    if (str_contains($component, $pista)) {
                        $suena = true;

                        break;
                    }
                }

                return [
                    'component' => $fila->component,
                    'name' => $fila->name,
                    'entornos' => (int) $fila->entornos,
                    'suena' => $suena,
                ];
            })
            // Los que suenan a nuestros primero; después, los más repartidos.
            ->sortByDesc(fn (array $fila) => [$fila['suena'], $fila['entornos']])
            ->take(self::CUANTAS_SUGERENCIAS)
            ->values();
    }

    public function render()
    {
        return view('livewire.configurations.own-plugins', [
            'declarados' => $this->declarados(),
            'porSerProducto' => $this->porSerProducto(),
            'sugerencias' => $this->sugerencias(),
        ])->layout('layouts.app');
    }
}
