<?php

namespace App\Livewire\Setups;

use App\Models\Products\Product;
use App\Models\Setups\Setup;
use App\Support\PeticionesDeContenido;
use App\Support\TiposDeContenido;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use App\Support\RepartoDeVersiones;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public Product $product;
    public string $search = '';
    public ?string $statusFilter = null;

    protected $queryString = ['search', 'statusFilter'];

    public function updatingSearch()
    {
        $this->resetPage();
    }

    public function updatingStatusFilter()
    {
        $this->resetPage();
    }

    /**
     * Abre la modal del cambio de estado.
     *
     * **Deprecar un setup deja de servírselo a quien lo esté recibiendo**, y antes esto era
     * un `confirm()` del navegador con el número metido a mano en el atributo. Dos cosas
     * quedaban fuera: a **qué versión** cae cada entorno —no todos caen a la misma, depende
     * de la versión de su plugin— y **cuáles se quedan sin ninguna**, que es el caso que
     * hay que mirar porque la API responde «sin contenido» y eso no avisa a nadie.
     *
     * La misma modal que los otros seis tipos, que además la enseña con el diseño del
     * panel y no con la alerta del navegador.
     */
    public function pedirCambioDeEstado(int $id, string $estado): void
    {
        // Permiso de ESCRITURA, aunque la ruta de esta pantalla exija solo
        // `admin.setups.index`: cambiar el estado de un setup es editar. Los roles que
        // hoy tienen `index` tienen también `edit`, así que no cambia nada para nadie.
        // Ver known-issues MGR-005.
        $this->authorize('admin.setups.edit');

        $this->dispatch('openModal',
            component: \App\Livewire\Products\CambioDeEstadoModal::class,
            arguments: [
                'tipo' => 'setups',
                'productId' => $this->product->id,
                'versionId' => $this->setupDelProducto($id)->id,
                'estado' => $estado,
            ]
        );
    }

    /** La modal avisa al aplicar; con recibirlo se repinta el listado y sus contadores. */
    #[\Livewire\Attributes\On('versionCambioDeEstado')]
    public function repintarTrasElCambio(): void
    {
        //
    }

    /**
     * Abre la modal de duplicar. Ver `HasVersionIndex::pedirDuplicar()`.
     */
    public function pedirDuplicar(int $id): void
    {
        $this->authorize('admin.setups.create');

        $this->dispatch('openModal',
            component: \App\Livewire\Products\DuplicarVersionModal::class,
            arguments: [
                'tipo' => 'setups',
                'productId' => $this->product->id,
                'versionId' => $this->setupDelProducto($id)->id,
            ]
        );
    }

    /** La modal avisa al crear, y con eso el listado se repinta con el setup nuevo. */
    #[\Livewire\Attributes\On('versionDuplicada')]
    public function repintarTrasDuplicar(): void
    {
        $this->resetPage();
    }

    public function duplicate($id)
    {
        $this->authorize('admin.setups.create');

        // La búsqueda va FUERA del try a propósito: `firstOrFail` lanza
        // `ModelNotFoundException`, y dentro del try la capturaba el `catch (\Exception)`
        // de abajo y acababa pintándole al usuario "No query results for model
        // [App\Models\Setups\Setup]". Un setup que no es de este producto no es un
        // error al duplicar: es un 404.
        $setup = $this->setupDelProducto($id);

        try {
            $newSetup = $setup->duplicate();

            session()->flash('success', "Setup duplicado. Nueva versión: {$newSetup->version}");

            return $this->redirect(route('products.setups.edit', [$this->product, $newSetup]), navigate: true);
        } catch (\Exception $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    public function mount(Product $product)
    {
        $this->product = $product;
    }

    /**
     * El setup que llega por id, comprobando que es de ESTE producto.
     *
     * `Setup::findOrFail($id)` a secas era un IDOR (known-issues MGR-006): esta
     * pantalla vive bajo `/products/{product}/setups`, el id llega del cliente, y
     * nadie comprobaba que el setup perteneciera al producto de la URL. Con el id de
     * un setup de otro producto se le cambiaba el estado o se duplicaba —y el
     * duplicado se creaba dentro del producto ajeno—.
     *
     * Se resuelve acotando la consulta al producto en vez de comparando después: si
     * no es suyo, es un 404, que es lo que le corresponde a un recurso que para esta
     * pantalla no existe.
     */
    private function setupDelProducto($id): Setup
    {
        return Setup::where('product_id', $this->product->id)
            ->where('id', $id)
            ->firstOrFail();
    }

    /**
     * Qué hay dentro del YAML, en una línea.
     *
     * **El YAML es el contenido de un setup**, y el listado no enseñaba nada de él: solo
     * la versión y la fecha. 181 líneas no caben en una fila, pero «7 claves · 2 roles · 4
     * campos de curso» sí, y es lo que permite ver de un golpe que una versión creció o
     * que le falta una sección respecto a la anterior.
     *
     * Se parsea con el mismo `Yaml::parse` que usa la API al servirlo, así que si el YAML
     * está mal escrito **aquí se ve antes de que un cliente se lo lleve**: un setup que no
     * parsea es un error 500 en su Moodle.
     *
     * @return array{claves:int, resumen:string, roto:bool}
     */
    private function resumenDelYaml(?string $yaml): array
    {
        if (trim((string) $yaml) === '') {
            return ['claves' => 0, 'resumen' => 'YAML vacío', 'roto' => false];
        }

        try {
            $datos = Yaml::parse($yaml);
        } catch (ParseException $e) {
            // Que no parsee es lo peor que le puede pasar a un setup: la API le devolvería
            // un 500 al cliente. Se dice aquí y en rojo.
            return ['claves' => 0, 'resumen' => 'El YAML no se puede leer', 'roto' => true];
        }

        if (! is_array($datos)) {
            return ['claves' => 0, 'resumen' => 'El YAML no describe una configuración', 'roto' => true];
        }

        // Las piezas que de verdad se cuentan: son las que el plugin crea en el Moodle del
        // cliente, y las que alguien quiere comparar entre dos versiones.
        $piezas = [];
        $lineas = count(explode("\n", (string) $yaml));
        $piezas[] = $lineas . ' ' . ($lineas === 1 ? 'línea' : 'líneas');

        foreach ([
            'roles' => ['rol', 'roles'],
            'users' => ['usuario', 'usuarios'],
        ] as $clave => [$singular, $plural]) {
            $cuantos = is_array($datos[$clave] ?? null) ? count($datos[$clave]) : 0;

            if ($cuantos > 0) {
                $piezas[] = $cuantos . ' ' . ($cuantos === 1 ? $singular : $plural);
            }
        }

        // Los campos personalizados van anidados bajo `fields`, que es donde están de
        // verdad: contar la clave de primer nivel diría «1 campo» habiendo cuatro.
        foreach (['course_custom_fields' => 'de curso', 'user_custom_fields' => 'de usuario'] as $clave => $de) {
            $campos = $datos[$clave]['fields'] ?? null;

            if (is_array($campos) && $campos !== []) {
                $piezas[] = count($campos) . ' ' . (count($campos) === 1 ? 'campo' : 'campos') . ' ' . $de;
            }
        }

        if (isset($datos['webservice'])) {
            $piezas[] = 'servicio web';
        }

        return [
            'claves' => count($datos),
            'resumen' => 'YAML · ' . implode(' · ', $piezas),
            'roto' => false,
        ];
    }

    /**
     * Qué setup está recibiendo cada cliente.
     *
     * **Solo cuentan los activos.** Un setup `deprecated` no se sirve por la API, así que
     * un cliente cuyo plugin resolvería a él recibe en realidad el siguiente activo por
     * debajo —o un error, si no hay ninguno—. Calcular el reparto sobre todos diría que
     * un setup deprecado le está llegando a alguien, que es justo lo contrario de lo que
     * significa deprecarlo.
     */
    private function reparto(): array
    {
        return RepartoDeVersiones::reparto(
            $this->product,
            Setup::where('product_id', $this->product->id)->active()->pluck('version')
        );
    }

    public function render()
    {
        $setups = Setup::query()
            ->with(['creator', 'product'])
            ->where('product_id', $this->product->id)
            ->when($this->search, function ($query) {
                $query->where('version', 'like', "%{$this->search}%");
            })
            ->when($this->statusFilter, function ($query) {
                if ($this->statusFilter === 'active') {
                    $query->active();
                } elseif ($this->statusFilter === 'deprecated') {
                    $query->deprecated();
                }
            })
            ->orderByDesc('version')
            ->paginate(20);

        // Lo que ha pasado de verdad: quién ha llamado a la acción `setup`, con qué
        // versión y si le fue bien.
        $peticiones = PeticionesDeContenido::de($this->product, 'setup');

        // El resumen del YAML de cada uno, en la propia colección: la vista no debería
        // parsear nada.
        $setups->getCollection()->transform(function (Setup $setup) {
            $setup->resumenYaml = $this->resumenDelYaml($setup->yaml);

            return $setup;
        });

        return view('livewire.setups.index', [
            'setups' => $setups,
            'product' => $this->product,
            'reparto' => $this->reparto(),
            'peticiones' => $peticiones,
            // **Una fila por cliente**, con el cálculo y el registro juntos. Solo los
            // activos: un deprecado no se sirve.
            'porEntorno' => RepartoDeVersiones::porEntorno(
                $this->product,
                Setup::where('product_id', $this->product->id)->active()->pluck('version'),
                $peticiones,
                Setup::where('product_id', $this->product->id)->active()->pluck('id', 'version')->all()
            ),
            'tipos' => TiposDeContenido::paraElSelector($this->product, 'setups'),
            'ficha' => array_merge(TiposDeContenido::TIPOS['setups'], [
                'fondo' => TiposDeContenido::tonoDe('setups')[0],
                'tinta' => TiposDeContenido::tonoDe('setups')[1],
                // Para que la tabla de entornos pueda enlazar al setup que le corresponde.
            'rutaVer' => 'products.setups.show',
            'explicacion' => 'La configuración que el plugin aplica en la plataforma del cliente: campos de perfil, roles, usuarios y servicios web. La pide su propio Moodle al sincronizar y se aplica de forma idempotente —volver a aplicarla no duplica nada—. Solo se sirven los activos.',
            ]),
            // Los recuentos van sobre TODOS los del producto, no sobre lo filtrado: un
            // contador que se mueve al filtrar no sirve para decidir qué filtrar.
            'cuantosActivos' => Setup::where('product_id', $this->product->id)->active()->count(),
            'cuantosDeprecados' => Setup::where('product_id', $this->product->id)->deprecated()->count(),
            'puedeEscribir' => auth()->user()?->can('admin.setups.edit') ?? false,
            'puedeCrear' => auth()->user()?->can('admin.setups.create') ?? false,
        ])->layout('layouts.app');
    }
}


