<?php

namespace App\Livewire\Configurations\Manager;

use App\Livewire\Components\ConfirmModal;
use App\Models\ApiRequestLog;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\ApiRateEvent;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Services\System\ApiLogPurger;
use App\Services\System\Settings;
use App\Support\LimiteDeSubida;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * Ajustes del Manager — reorganizada desde `Configuración.dc.html`.
 *
 * **Qué había.** 802 líneas de blade, **cuatro pestañas propias dentro de una sección que
 * ya tenía pestañas** —pestañas dentro de pestañas— y seis botones de guardar. Con eso,
 * cambiar el destinatario de un aviso y bajar un tope de la API se parecían: mismo
 * formulario, mismo botón, mismo aspecto. Y no son lo mismo —lo primero no afecta a
 * ningún cliente, lo segundo puede empezar a devolver 429 a sitios que hasta ese momento
 * pasaban—.
 *
 * **Qué hay ahora.** Los 17 ajustes agrupados en tres bloques que salen de
 * `Settings::CATALOGO`, cada fila diciendo qué vale y **si es suyo o heredado del
 * `.env`** —lo que antes era invisible: varios umbrales están a `0`, que significa «usa
 * el del fichero», y eso no se distinguía de «lo he puesto a cero»—. Y se edita de uno en
 * uno, en `SettingModal`, que explica la consecuencia antes de guardar.
 *
 * **Aquí ya no se guarda nada.** Toda la escritura vive en `SettingModal`, así que hay un
 * solo sitio donde se valida cada tipo de ajuste. Lo que se queda en esta pantalla es lo
 * que **no es un ajuste**:
 *
 * - **Cuánto histórico hay en el log de la API** y qué se borraría esta noche. Sin ese
 *   dato, elegir la retención es adivinar.
 * - **Aplicar la retención ahora**, sin esperar a las 03:30 —y sin depender de que el
 *   planificador esté en el cron, el olvido clásico—. Exige `admin.api-logs.destroy`,
 *   que no es el permiso de configurar: decidir cuánto se guarda y borrarlo de golpe no
 *   son la misma cosa.
 * - **Lo que se ha observado de verdad** en los límites, para no elegir los topes a ojo.
 * - **Los bloqueos puestos a mano y la lista blanca del fichero**: son las dos listas que
 *   deciden quién no pasa y quién no se mira nunca.
 * - **El límite de subida de archivos, en lectura.** Cuatro capas pueden cortar una
 *   subida y el formulario solo conoce la suya, que es la última: de ahí
 *   PRODSECU-156, «el aviso dice 5MB pero al subir una de 3 no se pudo subir». Aquí
 *   se ven los cuatro números de golpe, y **el de nginx sale con un `?` a propósito**
 *   porque PHP no lo puede saber: cuando el servidor web corta, PHP no se ejecuta.
 * - **El SMTP, en lectura.** Las credenciales no se configuran aquí: un SMTP editable
 *   desde el panel deja al Manager sin poder enviar nada —incluidos los correos de
 *   recuperación de contraseña— si se guarda mal, y quien se queda fuera no puede entrar
 *   a arreglarlo.
 */
class Index extends Component
{
    /**
     * Al guardar un ajuste, la pantalla se vuelve a pintar con el valor nuevo.
     *
     * Y se olvidan las cachés que dependen de lo guardado: sin esto, cambiar la retención
     * dejaba el recuento diciendo lo de hace un minuto, y el ajuste parecía no haberse
     * aplicado.
     */
    #[On('settingsChanged')]
    public function refrescar(): void
    {
        app(Settings::class)->olvidar();

        Cache::forget('settings:limites:observado');
        Cache::forget('configs:portada:parque');
        Cache::forget('configs:portada:acceso');
        $this->olvidarElHistorico();
    }

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

        $this->dispatch('openModal',
            component: SettingModal::class,
            arguments: ['clave' => $clave]
        );
    }

    /**
     * Bloquear una IP a mano, escribiéndola.
     *
     * Se abre desde aquí porque **aquí es donde se ven los bloqueos manuales**: a esta
     * pantalla se llega sabiendo qué IP hay que cortar. El otro punto de entrada está
     * en el ranking de tráfico, y ese solo sirve para IPs que ya han llamado.
     */
    public function abrirBloqueoDeIp(): void
    {
        $this->authorize('admin.configs.edit');

        $this->dispatch('openModal',
            component: \App\Livewire\Monitoring\IpBlockModal::class,
            arguments: ['ip' => '']
        );
    }

    /** Tras bloquear una IP, la lista de bloqueos manuales se vuelve a pintar. */
    #[On('apiBlocksChanged')]
    public function refrescarBloqueos(): void
    {
        // `render()` recarga la lista; el listener existe para que el evento del modal
        // llegue a algún sitio.
    }

    /* ==========================================================
       Limpieza del log: ejecutarla ahora, sin esperar a la noche
       ========================================================== */

    /**
     * Pide confirmación para aplicar la retención **en este momento**.
     *
     * Sin esto, guardar una retención de 90 días no hacía nada visible hasta las 03:30 del
     * día siguiente: nadie sabía si había funcionado, ni cuánto iba a borrar. Y en un
     * servidor sin el planificador en el cron —el olvido clásico— no habría pasado nunca,
     * en silencio.
     *
     * El permiso que se exige es **`admin.api-logs.destroy`**, no el de configuración.
     */
    public function pedirLimpiezaAhora(): void
    {
        $this->authorize('admin.api-logs.destroy');

        $dias = app(Settings::class)->diasDeRetencionDeLogs();

        if ($dias === 0) {
            session()->flash('warning', 'No hay retención configurada: pon primero cuántos días '
                . 'quieres conservar. Para un borrado puntual, en Monitorización → Peticiones se '
                . 'puede elegir un rango de fechas.');

            return;
        }

        // La cuenta va ANTES de la confirmación: es la diferencia entre borrar 40 filas y
        // borrar dos millones, y es lo único que permite decidir de verdad.
        $total = ApiRequestLog::where('started_at', '<', now()->subDays($dias))->count();

        if ($total === 0) {
            session()->flash('info', 'No hay nada más antiguo de ' . $dias . ' días: no se borra nada.');

            return;
        }

        $this->dispatch('openModal',
            component: ConfirmModal::class,
            arguments: [
                'title' => 'Borrar ahora los logs antiguos',
                'message' => 'Se van a borrar ' . number_format($total, 0, ',', '.')
                    . ' registro(s), los anteriores a ' . $dias . ' días. Es la misma limpieza que '
                    . 'haría la tarea de esta noche, ejecutada ya. No se puede deshacer.',
                'confirmText' => 'Sí, borrar ahora',
                'cancelText' => 'Cancelar',
                'eventName' => 'logsPurgeNowConfirmed',
                'confirmButtonColor' => 'red',
                'itemId' => $dias,
                'itemName' => number_format($total, 0, ',', '.') . ' registro(s)',
            ]
        );
    }

    /**
     * Aplica la retención ahora mismo.
     *
     * Los días **se vuelven a leer de la configuración**, no del modal: entre abrir la
     * confirmación y aceptarla puede haber pasado cualquier cosa, y el número que manda es
     * siempre el guardado.
     */
    #[On('logsPurgeNowConfirmed')]
    public function limpiarLogsAhora($datos = null): void
    {
        $this->authorize('admin.api-logs.destroy');

        // OJO con la firma: `ConfirmModal` dispara el evento con un array de datos, así
        // que un parámetro con tipo aquí recibiría ESE array en vez de lo inyectado por
        // el contenedor. Los servicios se resuelven dentro.
        $dias = app(Settings::class)->diasDeRetencionDeLogs();

        if ($dias === 0) {
            return;
        }

        $borrados = app(ApiLogPurger::class)->borrar(
            fn () => ApiRequestLog::where('started_at', '<', now()->subDays($dias)),
            'anteriores a ' . $dias . ' días, a mano',
            'la configuración'
        );

        // El recuento de la pantalla está cacheado un minuto y se quedaría diciendo que
        // aún hay lo que acabamos de borrar.
        $this->olvidarElHistorico();

        session()->flash('success', number_format($borrados, 0, ',', '.')
            . ' registro(s) borrados. La tarea de cada noche sigue activa.');
    }

    /**
     * Levanta un bloqueo manual desde la propia pantalla de configuración.
     *
     * El listado de bloqueos manuales vive aquí porque es **configuración**: alguien
     * decidió que esas IPs no pasan, y esa decisión se revisa donde se toma. Poder verlas
     * y no poder quitarlas obligaría a irse a Monitorización a buscar la fila.
     *
     * No borra la fila: la marca como revisada, con quién y cuándo. Un bloqueo que
     * desaparece sin dejar rastro es un bloqueo que nadie puede explicar después.
     */
    public function quitarBloqueoManual(int $id): void
    {
        $this->authorize('admin.configs.edit');

        $bloqueo = ApiBlock::where('reason', ApiBlock::REASON_MANUAL)->findOrFail($id);

        if ($bloqueo->state === ApiBlock::STATE_CLEARED) {
            return;
        }

        $bloqueo->liberar(Auth::id(), 'Levantado desde la configuración de bloqueos');

        MonitoringLog::db(
            'warning',
            '16011',
            'Bloqueo manual levantado para ' . ($bloqueo->ip ?? 'sin IP')
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')'
        );

        session()->flash('success', 'La IP ' . $bloqueo->ip . ' ya no está bloqueada a mano.');
    }

    /* ==========================================================
       Los datos de la pantalla
       ========================================================== */

    /**
     * Lo que se ha observado de verdad, para no elegir los topes a ojo.
     *
     * Sale de `api_rate_events`, que el middleware ya escribe agregado por IP y ventana de
     * un minuto: es una tabla pequeña y la consulta es portable, al contrario que agrupar
     * `api_request_logs` por minuto —que exige funciones de fecha distintas en MariaDB y
     * en sqlite—.
     *
     * **Ojo con lo que NO dice**: solo hay filas a partir del margen de aviso, así que
     * «ninguna incidencia» significa «nadie ha pasado del margen», no «el tope es
     * correcto». La pantalla lo dice.
     *
     * @return array<string, array{max: int, ip: string|null, ips: int}>
     */
    private function loObservado(): array
    {
        return Cache::remember('settings:limites:observado', 300, function () {
            $desde = now()->subDays(7);
            $resultado = [];

            foreach ([ApiRateEvent::LIMIT_FAILURES, ApiRateEvent::LIMIT_REQUESTS] as $tipo) {
                $fila = ApiRateEvent::where('limit_kind', $tipo)
                    ->where('window_started_at', '>=', $desde)
                    ->orderByDesc('observed')
                    ->first();

                $resultado[$tipo] = [
                    'max' => (int) ($fila->observed ?? 0),
                    'ip' => $fila->ip ?? null,
                    'ips' => ApiRateEvent::where('limit_kind', $tipo)
                        ->where('window_started_at', '>=', $desde)
                        ->distinct()
                        ->count('ip'),
                ];
            }

            return $resultado;
        });
    }

    /**
     * Qué hay en el log ahora mismo, y qué se borraría con la retención puesta.
     *
     * El segundo número es el que importa: es la diferencia entre guardar un ajuste y
     * enterarse de que esa noche desaparecen dos millones de filas.
     *
     * Cacheado un minuto porque en Livewire `render()` se ejecuta en cada interacción y
     * esto son tres agregados sobre la tabla más grande del sistema.
     */
    private function historicoDeLogs(): array
    {
        $dias = app(Settings::class)->diasDeRetencionDeLogs();

        return Cache::remember($this->claveDelHistorico($dias), 60, function () use ($dias) {
            $primero = ApiRequestLog::min('started_at');

            return [
                'total' => ApiRequestLog::count(),
                'desde' => $primero ? \Carbon\Carbon::parse($primero) : null,
                'a_borrar' => $dias > 0
                    ? ApiRequestLog::where('started_at', '<', now()->subDays($dias))->count()
                    : 0,
            ];
        });
    }

    private function claveDelHistorico(int $dias): string
    {
        return 'settings:logs:historico:' . $dias;
    }

    /**
     * Se olvida el histórico de la retención guardada **y el de las candidatas**.
     *
     * La clave lleva los días dentro, así que al cambiar la retención la entrada vieja se
     * queda con el número anterior y la nueva se calcula sola. Se olvidan las dos: la
     * anterior también, porque si se vuelve atrás en el mismo minuto habría que ver el
     * recuento de verdad, no el de antes de borrar.
     */
    private function olvidarElHistorico(): void
    {
        foreach ([0, app(Settings::class)->diasDeRetencionDeLogs()] as $dias) {
            Cache::forget($this->claveDelHistorico($dias));
        }
    }

    public function render(Settings $ajustes)
    {
        return view('livewire.configurations.manager.index', [
            'grupos' => $ajustes->grupos(),
            'corta' => $ajustes->losLimitesCortan(),
            'observados' => ApiBlock::blocked()->where('enforced', false)->count(),
            // Los valores con los que la API está corriendo AHORA, que no son los del
            // panel: el panel tiene ceros donde manda el fichero.
            'umbrales' => $ajustes->umbralesDeLaApi(),
            'observado' => $this->loObservado(),
            'logs' => $this->historicoDeLogs(),
            'retencion' => $ajustes->diasDeRetencionDeLogs(),
            'manuales' => ApiBlock::where('reason', ApiBlock::REASON_MANUAL)
                ->orderByDesc('last_blocked_at')
                ->get(),
            'listaBlanca' => config('api.throttle.allowlist', []),
            // Los de soporte pueden venir heredados de licencias, así que la pantalla
            // tiene que decir a quién se va a avisar DE VERDAD, no qué hay en el campo.
            'destinatariosSoportes' => $ajustes->destinatariosDeSoportes(),
            'soportesHeredados' => $ajustes->correos(Settings::SOPORTES_EMAIL) === [],
            // Los cuatro topes de subida, para diagnosticar sin SSH. Ver PRODSECU-156.
            'subidas' => LimiteDeSubida::diagnostico(),
            'subidaEfectiva' => LimiteDeSubida::comoTexto(),
            'subidaZip' => LimiteDeSubida::comoTexto(LimiteDeSubida::ZIP_KB),
            // El SMTP en lectura: de dónde sale el correo, sin entrar al servidor.
            'smtp' => [
                'transporte' => config('mail.default'),
                'servidor' => config('mail.mailers.smtp.host'),
                'puerto' => config('mail.mailers.smtp.port'),
                'remitente' => config('mail.from.address'),
                'nombre' => config('mail.from.name'),
            ],
        ])->layout('layouts.app');
    }
}
