<?php

namespace App\Livewire\Configurations;

use App\Livewire\Components\ConfirmModal;
use App\Livewire\Traits\ProtectsSystemRoles;
use App\Models\Auth\User;
use App\Models\Monitoring\Log as MonitoringLog;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\On;
use Livewire\Attributes\Url;
use Livewire\Component;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

/**
 * Acceso: roles y permisos, en la misma pantalla — desde `Configuración.dc.html`.
 *
 * **Por qué juntos.** Eran dos pestañas y son la misma pregunta a dos niveles: «qué
 * alcanza este rol» y «quién tiene este permiso». Separadas, ninguna de las dos se
 * podía responder:
 *
 * - **«Roles» no decía qué alcanza cada rol.** Tres columnas —Nombre, Alias,
 *   Descripción— y ni el número de permisos ni cuánta gente lo usa. Con eso, **cuatro
 *   de los seis roles no tenían usuarios y era invisible**, y Support no tenía ni un
 *   permiso sin que ninguna pantalla lo dijera.
 * - **«Permisos» era una lista plana de 126 filas** con Permiso, Módulo y Descripción.
 *   No decía **qué roles tienen cada permiso**, que es la única pregunta que se hace
 *   mirando esa pantalla. Para saber qué puede hacer un Manager había que ir a Roles, y
 *   ahí tampoco se veía.
 *
 * Es el mismo movimiento que juntar «Bloqueos» e «IPs» en Monitorización.
 *
 * **Los 126 permisos los crea el seeder, no el panel.** Aquí solo se ven y se reparten;
 * crear o borrar permisos a mano rompe la correspondencia con el código, que es lo que
 * los mira en cada `@can` y en cada middleware.
 */
class Access extends Component
{
    use ProtectsSystemRoles;

    /**
     * Búsqueda de permiso o módulo.
     *
     * **En la URL** para poder mandar el enlace: «mira, `license-tokens` lo tiene
     * Manager» es una conversación que se tiene, y sin esto hay que explicar dónde
     * escribir.
     */
    #[Url(as: 'q', except: '')]
    public string $busca = '';

    /** Qué módulo está desplegado. Uno a la vez: son 27 y abiertos todos no se leen. */
    public ?string $abierto = 'clients';

    /**
     * Los colores de cada rol, los mismos en toda la pantalla.
     *
     * Están aquí y no en la vista porque las píldoras del listado de roles y las
     * iniciales de la columna «quién lo tiene» **tienen que coincidir**: si Manager es
     * verde arriba y azul abajo, la tabla de permisos deja de leerse de un vistazo.
     *
     * @var array<string, array{ini: string, fg: string, bg: string}>
     */
    public const COLORES = [
        'Admin' => ['ini' => 'A', 'fg' => 'var(--orange-600)', 'bg' => 'var(--orange-100)'],
        'Manager' => ['ini' => 'M', 'fg' => 'var(--teal-700)', 'bg' => 'var(--teal-100)'],
        'Developer' => ['ini' => 'D', 'fg' => 'var(--info-500)', 'bg' => 'var(--info-50)'],
        'Support' => ['ini' => 'S', 'fg' => 'var(--warning-500)', 'bg' => 'var(--warning-50)'],
        'System' => ['ini' => 'Sy', 'fg' => 'var(--neutral-700)', 'bg' => 'var(--surface-sunken)'],
        'WebService' => ['ini' => 'W', 'fg' => 'var(--neutral-600)', 'bg' => 'var(--surface-sunken)'],
    ];

    /**
     * El rol de la API. No navega el panel, así que su alcance no se cuenta igual.
     *
     * El mismo nombre que usa `Users\Index` para separar las cuentas de servicio de las
     * personas.
     */
    public const ROL_DE_SERVICIO = 'WebService';

    /** Un rol que no está en la tabla de arriba sigue necesitando colores. */
    public static function colorDe(string $rol): array
    {
        return self::COLORES[$rol] ?? [
            'ini' => mb_strtoupper(mb_substr($rol, 0, 1)),
            'fg' => 'var(--neutral-700)',
            'bg' => 'var(--surface-sunken)',
        ];
    }

    public function alternar(string $modulo): void
    {
        $this->abierto = $this->abierto === $modulo ? null : $modulo;
    }

    /* ==========================================================
       Borrar un rol
       ========================================================== */

    /**
     * Pide confirmación para borrar un rol.
     *
     * **Vive aquí porque el listado de roles vive aquí.** Estaba en
     * `Configurations\Roles\Index`, que era la pestaña «Roles» y ha desaparecido al
     * juntarse con permisos; sin mover esto, borrar un rol se habría quedado sin punto de
     * entrada —el patrón que este release lleva corrigiendo cuatro veces—.
     *
     * Se comprueba antes de abrir la modal y otra vez al confirmar. Las dos razones para
     * no borrar:
     *
     * - **Es un rol del sistema.** El código y la API dependen de los seis que crea el
     *   seeder (MGR-018).
     * - **Alguien lo tiene.** Borrarlo dejaría a esas personas sin ningún rol, o sea
     *   entrando al panel y sin ver nada, y sin ningún error que lo explique.
     */
    public function abrirBorradoDeRol(int $rolId): void
    {
        $this->authorize('admin.roles.destroy');

        $rol = Role::find($rolId);

        if ($rol === null) {
            session()->flash('error', 'Ese rol ya no existe.');

            return;
        }

        if (! $this->sePuedeBorrar($rol)) {
            session()->flash('error', 'El rol «' . $rol->name . '» lo crea el despliegue: borrarlo '
                . 'solo haría que volviera a aparecer vacío en la siguiente migración de permisos, '
                . 'con su gente por el camino. Sus permisos sí se pueden cambiar.');

            return;
        }

        $cuantos = User::role($rol->name)->count();

        if ($cuantos > 0) {
            session()->flash('error', 'No se puede borrar «' . $rol->name . '»: '
                . ($cuantos === 1 ? 'lo tiene 1 persona' : 'lo tienen ' . $cuantos . ' personas')
                . '. Sin rol entrarían al panel y no verían ninguna pantalla, sin ningún error '
                . 'que lo explicara. Cámbiales el rol primero, desde Usuarios.');

            return;
        }

        $this->dispatch('openModal',
            component: ConfirmModal::class,
            arguments: [
                'itemId' => $rol->id,
                'itemName' => $rol->fullname ?: $rol->name,
                'title' => 'Borrar el rol «' . ($rol->fullname ?: $rol->name) . '»',
                'message' => 'Nadie lo tiene y no es del sistema, así que no deja a ninguna '
                    . 'persona sin acceso. Se borra el rol y su reparto de '
                    . $rol->permissions()->count() . ' permiso(s). No se puede deshacer.',
                'confirmText' => 'Sí, borrar el rol',
                'cancelText' => 'Cancelar',
                'eventName' => 'roleDeleted',
                'context' => 'role',
                'confirmButtonColor' => 'red',
            ]
        );
    }

    /**
     * Borra el rol confirmado.
     *
     * Se vuelve a comprobar todo: entre abrir la modal y aceptarla alguien puede haberle
     * asignado el rol a una persona, y esconder el botón no es protegerlo (MGR-005).
     */
    #[On('roleDeleted')]
    public function borrarRol($datos = null): void
    {
        $this->authorize('admin.roles.destroy');

        // OJO con la firma: `ConfirmModal` dispara el evento con un array, así que un
        // parámetro con tipo aquí recibiría ESE array en vez de lo inyectado.
        $rolId = is_array($datos) ? ($datos['itemId'] ?? null) : $datos;
        $rol = Role::find((int) $rolId);

        if ($rol === null) {
            return;
        }

        if (! $this->sePuedeBorrar($rol) || User::role($rol->name)->count() > 0) {
            session()->flash('error', 'No se ha borrado «' . $rol->name . '»: o lo crea el '
                . 'despliegue o alguien lo tiene asignado.');

            return;
        }

        $nombre = $rol->fullname ?: $rol->name;
        $permisos = $rol->permissions()->count();

        $rol->delete();

        MonitoringLog::db(
            'warning',
            '16026',
            'Rol borrado: ' . $nombre . ' (' . $rol->name . ') — tenía ' . $permisos . ' permiso(s)'
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'Role',
            (string) $rol->id
        );

        // La portada cuenta roles y roles sin permisos, y está cacheada.
        Cache::forget('configs:portada:acceso');

        session()->flash('success', 'Rol «' . $nombre . '» borrado.');
    }

    /**
     * Los roles con su alcance, sus permisos y cuánta gente los tiene.
     *
     * El **alcance** es una frase, no un número: «67 permisos» no dice si un Manager ve
     * clientes. Sale de qué módulos alcanza el rol, y se calcula a partir de los
     * permisos que tiene —así no se queda desfasada cuando alguien cambia el reparto—.
     *
     * `withCount('users')` y no `User::role($nombre)->count()` en el bucle: lo segundo
     * era una consulta por rol, el N+1 de MGR-016 otra vez. Y con `withCount` en vez de
     * `DB::table('model_has_roles')` porque el test de seguridad marca cualquier
     * `DB::table(` como posible escritura —y hace bien—.
     *
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function roles()
    {
        return Role::withCount(['permissions', 'users'])
            ->with('permissions:id,name')
            ->orderBy('name')
            ->get()
            ->map(function (Role $rol) {
                $usuarios = (int) $rol->users_count;
                $color = self::colorDe($rol->name);

                return [
                    'id' => $rol->id,
                    'nombre' => $rol->name,
                    'descripcion' => $rol->description ?? $rol->fullname ?? '',
                    'permisos' => $rol->permissions_count,
                    'usuarios' => $usuarios,
                    'alcance' => $this->alcanceDe($rol),
                    // **Un rol sin permisos es un fallo, no una configuración.** Quien lo
                    // tenga entra y no ve una sola pantalla, sin ningún error visible.
                    'sinPermisos' => $rol->permissions_count === 0,
                    'sinUsuarios' => $usuarios === 0,
                    // **Solo Admin queda sin «Editar»**, porque es el único desde el que uno
                    // puede cerrarse la puerta por dentro (MGR-018). Los otros cinco del
                    // seeder sí se editan: el alcance de Soporte es una decisión de
                    // organización y no debería exigir un despliegue.
                    'delSistema' => $this->esDelSistema($rol),
                    // Borrar es otra pregunta: los seis los recrea el despliegue.
                    'sePuedeBorrar' => $this->sePuedeBorrar($rol),
                    'fg' => $color['fg'],
                    'bg' => $color['bg'],
                ];
            });
    }


    /**
     * Qué alcanza un rol, dicho en una frase.
     *
     * Se deriva de los permisos que tiene ahora mismo, no de una tabla escrita a mano:
     * una descripción fija se queda mintiendo en cuanto alguien cambia el reparto, y
     * este panel ya ha tenido ese problema con el rol Support.
     */
    private function alcanceDe(Role $rol): string
    {
        if ($rol->permissions_count === 0) {
            return 'No ve ninguna pantalla: el rol no tiene permisos';
        }

        if ($rol->name === self::ROL_DE_SERVICIO) {
            // Sus permisos existen para que la API funcione, no para navegar. Enumerar
            // sus secciones haría creer que alguien con este rol entra al panel y ve
            // entornos, y no es eso.
            return 'Rol de la API: no navega el panel';
        }

        $modulos = $rol->permissions
            ->pluck('name')
            ->map(fn (string $permiso) => $this->moduloDe($permiso))
            ->unique();

        // Las secciones que se nombran, en el orden en el que se leen. Se dicen las que
        // alcanza y **las que no**, que es lo que distingue un Manager de un Admin.
        //
        // «gestión de usuarios» y no «usuarios» a propósito: la frase se completa con
        // «sin …», y «sin usuarios» se leería como «este rol no lo tiene nadie», que es
        // justo el dato de la columna de al lado.
        $secciones = [
            'Dashboard' => ['dashboard'],
            'clientes' => ['clients'],
            'entornos' => ['environments', 'supports'],
            'licencias' => ['license-tokens'],
            'contenido' => ['products', 'setups', 'scss', 'js', 'features', 'resources', 'tutorials'],
            'monitorización' => ['api-logs', 'monitoring', 'logs'],
            'gestión de usuarios' => ['users'],
            'configuración' => ['configs', 'configurations', 'roles', 'permissions', 'types'],
        ];

        $tiene = [];
        $noTiene = [];

        foreach ($secciones as $etiqueta => $claves) {
            if ($modulos->intersect($claves)->isNotEmpty()) {
                $tiene[] = $etiqueta;

                continue;
            }

            $noTiene[] = $etiqueta;
        }

        if ($noTiene === []) {
            return 'Todo el panel';
        }

        // «Solo contenido» dice mucho más que «Contenido» cuando el rol llega a una o dos
        // secciones de ocho: la palabra que informa es la que acota.
        $frase = count($tiene) <= 2
            ? 'Solo ' . implode(' y ', $tiene)
            : ucfirst(implode(', ', $tiene));

        // Solo se enumera lo que falta cuando son pocas cosas: «sin clientes, sin
        // entornos» ayuda; siete negaciones seguidas no se leen.
        if (count($noTiene) <= 3) {
            $frase .= ' — sin ' . implode(', sin ', $noTiene);
        }

        return $frase;
    }

    /** ¿Es Admin, el único rol cuyos permisos no se tocan desde el panel? */
    private function esDelSistema(Role $rol): bool
    {
        return $this->isSystemRole($rol);
    }

    /**
     * ¿Se puede borrar este rol?
     *
     * **Borrar no es editar**, y por eso se pregunta aparte desde que los permisos de los
     * cinco roles no blindados sí se editan. Los seis del seeder no se borran —el seeder los
     * volvería a crear vacíos y los usuarios se quedarían sin nada por el camino— y ninguno
     * con gente asignada tampoco.
     */
    private function sePuedeBorrar(Role $rol): bool
    {
        return ! $this->tieneNombreFijo($rol);
    }

    /** El módulo de un permiso: `admin.clients.index` → `clients`. */
    private function moduloDe(string $permiso): string
    {
        $partes = explode('.', $permiso);

        // `admin.configurations.products.index` tiene el módulo en la tercera parte;
        // el resto lo tiene en la segunda.
        if (($partes[1] ?? '') === 'configurations' && isset($partes[2])) {
            return 'configurations';
        }

        return $partes[1] ?? $permiso;
    }

    /**
    /**
     * Los permisos agrupados por módulo, y **qué roles tiene cada uno**.
     *
     * Son 27 módulos con 3–5 acciones cada uno: en lista plana no se lee, agrupados sí.
     *
     * Los roles de cada permiso vienen por **eager loading**: `with('roles:id,name')` son
     * dos consultas para los 126 permisos. Con `$permiso->roles` por fila serían 126.
     *
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function modulos()
    {
        $busca = mb_strtolower(trim($this->busca));

        return Permission::with('roles:id,name')
            ->orderBy('name')
            ->get()
            ->filter(function (Permission $permiso) use ($busca) {
                if ($busca === '') {
                    return true;
                }

                return str_contains(mb_strtolower($permiso->name), $busca)
                    || str_contains(mb_strtolower((string) $permiso->description), $busca);
            })
            ->groupBy(fn (Permission $permiso) => $this->moduloDe($permiso->name))
            ->map(function ($permisos, $modulo) {
                return [
                    'nombre' => $modulo,
                    'n' => $permisos->count(),
                    // Con búsqueda activa, todo abierto: si no, hay que desplegar a mano
                    // el módulo que el buscador acaba de encontrar.
                    'abierto' => $this->busca !== '' || $this->abierto === $modulo,
                    'permisos' => $permisos->map(function (Permission $permiso) {
                        $roles = $permiso->roles->pluck('name')->sort()->values()->all();

                        return [
                            'id' => $permiso->id,
                            'clave' => $permiso->name,
                            'desc' => $permiso->description ?: '—',
                            'roles' => array_map(
                                fn (string $rol) => ['nombre' => $rol] + self::colorDe($rol),
                                $roles
                            ),
                            // **Un permiso que ningún rol tiene es código muerto o una
                            // funcionalidad sin dueño.** Se marca porque son las pistas
                            // del roadmap: hay permisos en el seeder sin pantalla.
                            'sinRoles' => $roles === [],
                        ];
                    })->values(),
                ];
            })
            ->sortKeys()
            ->values();
    }

    public function render()
    {
        return view('livewire.configurations.access', [
            'roles' => $this->roles(),
            'modulos' => $this->modulos(),
            'totalPermisos' => Permission::count(),
        ])->layout('layouts.app');
    }
}
