<?php

namespace App\Livewire\Monitoring;

use App\Services\System\Settings;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Livewire\Component;

/**
 * Cuántas filas tiene cada tabla, y cuáles crecen sin que nadie las borre.
 *
 * **Por qué hace falta.** «¿Cuánto ocupa esto?» no se podía contestar sin entrar al servidor,
 * y es la pregunta que decide cosas concretas: cuánto va a tardar un `ALTER TABLE` en un
 * despliegue, si conviene poner una retención, o si algo que debería ser pequeño ha crecido
 * porque una tarea no está corriendo.
 *
 * **Y por qué no es solo una lista de números.** Un recuento suelto no dice nada: 400.000
 * filas en el registro de peticiones son normales y 400.000 en la tabla de avisos serían un
 * problema. Lo que se enseña al lado de cada número es **qué la hace crecer y qué la borra**,
 * que es lo que convierte un número en una decisión.
 *
 * Tres estados, y el tercero es el que importa:
 *
 * - **Con retención**: hay una tarea que la limpia y un plazo configurado.
 * - **Acotada**: no crece con el tiempo —se reemplaza en cada sincronización, o tiene un
 *   techo natural—, así que no necesita retención.
 * - **Sin límite**: crece y no la borra nada. Puede estar bien —un catálogo de veinte filas
 *   no es un problema— pero hay que verlo, no suponerlo.
 *
 * Es de solo lectura: aquí no se borra nada. La limpieza vive donde vive cada cosa —el visor
 * de peticiones, la modal de cortes, las retenciones de Configuraciones— porque borrar sin el
 * contexto de lo que se borra es justo lo que no se quiere ofrecer desde una lista de
 * números.
 */
class Tamano extends Component
{
    /**
     * Lo que se sabe de cada tabla.
     *
     * **Escrito a mano y no descubierto del esquema.** Listar las tablas es fácil; decir cuál
     * crece con el tráfico y cuál se reemplaza entera en cada `sync` no sale de ninguna
     * consulta. Y una tabla nueva que no esté aquí sale igual, en su grupo de «sin
     * clasificar», que es mejor que esconderla.
     *
     * @var array<string, array{que: string, limpieza: string, estado: string}>
     */
    private const FICHAS = [
        'api_request_logs' => [
            'que' => 'Una fila por cada petición que llega a la API',
            'limpieza' => 'Tarea diaria a las 03:30, con el plazo de «Retención del log de la API»',
            'estado' => 'retencion',
        ],
        'api_rate_events' => [
            'que' => 'Una fila por ventana de un minuto que cruzó el margen de aviso',
            'limpieza' => 'Tarea diaria a las 03:45, y a mano desde Tráfico y cortes',
            'estado' => 'retencion',
        ],
        'api_blocks' => [
            'que' => 'Una fila por episodio de corte. Los rechazos van dentro, en un contador',
            'limpieza' => 'Tarea diaria a las 03:45 (solo los revisados), y a mano desde Tráfico y cortes',
            'estado' => 'retencion',
        ],
        'notices' => [
            'que' => 'Un aviso enviado. No se repite: se reconoce por su referencia',
            'limpieza' => 'Ninguna. Crece con los incidentes, no con el tráfico',
            'estado' => 'sinlimite',
        ],
        'logs' => [
            'que' => 'Cada acción que alguien hace en el panel',
            'limpieza' => 'Ninguna. Es el rastro de quién hizo qué',
            'estado' => 'sinlimite',
        ],
        'plugins' => [
            'que' => 'El inventario de plugins de cada entorno',
            'limpieza' => 'Se reemplaza entera en cada sincronización del entorno',
            'estado' => 'acotada',
        ],
        'updates' => [
            'que' => 'Las actualizaciones de core que anuncia cada entorno',
            'limpieza' => 'Se reemplaza entera en cada sincronización',
            'estado' => 'acotada',
        ],
        'environments' => ['que' => 'Los entornos dados de alta', 'limpieza' => 'A mano', 'estado' => 'acotada'],
        'clients' => ['que' => 'Los clientes', 'limpieza' => 'A mano', 'estado' => 'acotada'],
        'license_tokens' => ['que' => 'Las licencias', 'limpieza' => 'A mano', 'estado' => 'acotada'],
        'products' => ['que' => 'Los productos', 'limpieza' => 'A mano', 'estado' => 'acotada'],
        'users' => ['que' => 'Las cuentas del panel', 'limpieza' => 'A mano', 'estado' => 'acotada'],
    ];

    /** A partir de aquí una tabla se considera grande y se marca. */
    public const MUCHAS_FILAS = 100000;

    public function mount(): void
    {
        // El gate de la ruta no se reaplica en /livewire/update (MGR-005).
        $this->authorize('admin.api-logs.index');
    }

    /**
     * Todas las tablas con su recuento, de la más grande a la más pequeña.
     *
     * **Cacheado cinco minutos.** Son tantos `count(*)` como tablas, y sobre las grandes eso
     * no es gratis. Nadie necesita este dato al segundo: se mira para decidir una retención o
     * para estimar un despliegue.
     *
     * @return array<int, array<string, mixed>>
     */
    public function tablas(): array
    {
        // **Es público, así que se comprueba.** Cualquier método público de un componente
        // Livewire se puede llamar desde `/livewire/update` sin pasar por la ruta (MGR-005),
        // y este devuelve el nombre de todas las tablas de la base con sus recuentos: no
        // escribe nada, pero es información.
        $this->authorize('admin.api-logs.index');

        return Cache::remember('monitoring:tamano', 300, function () {
            $filas = [];

            foreach ($this->nombresDeTablas() as $tabla) {
                $ficha = self::FICHAS[$tabla] ?? null;

                $filas[] = [
                    'tabla' => $tabla,
                    'filas' => (int) DB::table($tabla)->count(),
                    'que' => $ficha['que'] ?? null,
                    'limpieza' => $ficha['limpieza'] ?? null,
                    // Sin ficha no se inventa nada: se dice que no está clasificada, que es
                    // la verdad y además es accionable —alguien la añade—.
                    'estado' => $ficha['estado'] ?? 'sinclasificar',
                ];
            }

            usort($filas, fn (array $a, array $b) => $b['filas'] <=> $a['filas']);

            return $filas;
        });
    }

    /**
     * Los nombres de las tablas de la base, sin las de fontanería.
     *
     * `migrations`, `cache`, `jobs` y compañía no son datos del Manager: son del framework, y
     * verlas aquí solo alarga la lista.
     *
     * @return array<int, string>
     */
    private function nombresDeTablas(): array
    {
        $fuera = [
            'migrations', 'cache', 'cache_locks', 'jobs', 'job_batches', 'failed_jobs',
            'sessions', 'password_reset_tokens',
        ];

        return collect(Schema::getTableListing())
            // Según el driver, `getTableListing()` puede devolver `esquema.tabla`.
            ->map(fn (string $t) => str_contains($t, '.') ? substr($t, strrpos($t, '.') + 1) : $t)
            ->reject(fn (string $t) => in_array($t, $fuera, true))
            ->sort()
            ->values()
            ->all();
    }

    /**
     * El total, y cuántas tablas crecen sin que nadie las borre.
     *
     * @return array{filas: int, tablas: int, sinLimite: int, sinClasificar: int, grandes: int}
     */
    public function resumen(): array
    {
        $tablas = $this->tablas();

        return [
            'filas' => array_sum(array_column($tablas, 'filas')),
            'tablas' => count($tablas),
            // **Solo las clasificadas como «sin límite», y solo con filas dentro.**
            //
            // Antes entraban también las «sin clasificar», y eso daba «28 tablas crecen sin
            // que nada las borre» cuando casi todas son catálogos de veinte filas —permisos,
            // roles, tipos— que no crecen con nada. Un aviso así enseña a no leer los
            // avisos. Que una tabla no esté clasificada se ve en su fila, que es donde
            // corresponde: es una tarea, no una alarma.
            'sinLimite' => count(array_filter(
                $tablas,
                fn (array $t) => $t['estado'] === 'sinlimite' && $t['filas'] > 0
            )),
            // Las que nadie ha clasificado todavía, dicho aparte y sin color de alarma.
            'sinClasificar' => count(array_filter(
                $tablas,
                fn (array $t) => $t['estado'] === 'sinclasificar' && $t['filas'] > 0
            )),
            'grandes' => count(array_filter($tablas, fn (array $t) => $t['filas'] >= self::MUCHAS_FILAS)),
        ];
    }

    /**
     * Las retenciones configuradas, para decirlas al lado de las tablas que las usan.
     *
     * **Un 0 aquí es «no se borra nada»**, y es lo que hay que poder ver de un vistazo: la
     * tarea se ejecuta cada noche igual y no toca nada.
     *
     * @return array<string, int>
     */
    public function retenciones(Settings $ajustes): array
    {
        return [
            'api_request_logs' => $ajustes->diasDeRetencionDeLogs(),
            'api_blocks' => $ajustes->diasDeRetencionDeCortes(),
            'api_rate_events' => $ajustes->diasDeRetencionDeVentanas(),
        ];
    }

    /** Vuelve a contar: los recuentos se guardan cinco minutos. */
    public function recontar(): void
    {
        $this->authorize('admin.api-logs.index');

        Cache::forget('monitoring:tamano');
    }

    public function render(Settings $ajustes)
    {
        return view('livewire.monitoring.tamano', [
            'tablas' => $this->tablas(),
            'resumen' => $this->resumen(),
            'retenciones' => $this->retenciones($ajustes),
        ])->layout('layouts.app');
    }
}
