<?php

namespace App\Livewire\Monitoring;

use App\Models\ApiRequestLog;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\Log as MonitoringLog;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use LivewireUI\Modal\ModalComponent;

/**
 * Bloquear una IP, eligiendo si corta o solo se observa.
 *
 * **Lo que había** era un formulario suelto en la pantalla de IPs: la IP escrita a mano,
 * el motivo opcional y el corte siempre aplicado. Faltaban las dos cosas que el diseño
 * añade:
 *
 * - **Elegir el modo.** «Cortar» responde 403 desde la siguiente petición; «solo
 *   observar» registra el bloqueo y **no corta**. El segundo existe para poder mirar el
 *   efecto que tendría un corte antes de aplicarlo, y confundirlos hace pensar que se
 *   está cortando a un cliente cuando no —o al revés—.
 * - **El motivo obligatorio.** Un bloqueo manual no caduca solo: se levanta cuando
 *   alguien decide que sobra, y esa decisión se toma leyendo el motivo meses después.
 *
 * Y se enseña **qué tráfico tiene esa IP** antes de bloquearla: cuántas peticiones, de
 * cuántos dominios y cuántas rechazadas. Un solo dominio con muchas rechazadas es un
 * sitio en bucle; muchos dominios es un servidor compartido, y ahí bloquear corta a
 * varios clientes a la vez.
 */
class IpBlockModal extends ModalComponent
{
    public string $ip = '';

    /**
     * Con qué IP se abrió. Vacía si se escribe aquí.
     *
     * Hace falta aparte de `$ip`, que cambia en cuanto se teclea: sin esto, el campo
     * dejaría de ser editable en la primera letra.
     */
    public string $ipDeOrigen = '';

    /** cortar | observar */
    public string $modo = 'cortar';

    public string $motivo = '';

    /**
     * La IP puede venir dada o escribirse aquí.
     *
     * **Sin el segundo caso no se podía bloquear a mano de verdad.** El botón
     * «Bloquear…» solo existía en las filas del ranking de tráfico, así que solo se
     * podía bloquear a quien ya estuviera en la lista: una IP que aún no ha llamado
     * —la de un escaneo que empieza, la que te ha dado Sistemas— no había manera de
     * meterla. Es el mismo patrón de siempre: la lógica estaba y el punto de entrada no.
     */
    public function mount(string $ip = ''): void
    {
        // El permiso de la ruta no se reaplica en /livewire/update. Ver MGR-005.
        $this->authorize('admin.configs.edit');

        $this->ip = $ip;
        $this->ipDeOrigen = $ip;
    }

    /** ¿Se ha escrito la IP aquí, o venía del ranking? */
    public function seEscribeLaIp(): bool
    {
        return $this->ipDeOrigen === '';
    }

    public static function modalMaxWidth(): string
    {
        return '2xl';
    }

    /**
     * Qué está haciendo esa IP en los últimos 7 días.
     *
     * Una sola consulta agregada sobre `api_request_logs`, filtrada por la IP —que tiene
     * índice— y por fecha. Es el contexto que hace que la decisión no sea a ciegas.
     *
     * @return array{peticiones:int, errores:int, dominios:int, hosts:array<int, string>}
     */
    public function trafico(): array
    {
        $fila = ApiRequestLog::query()
            ->select([
                DB::raw('count(*) as peticiones'),
                DB::raw("sum(case when severity = '" . \App\Services\Api\Severidad::ERROR . "' then 1 else 0 end) as errores"),
                DB::raw('count(distinct host) as dominios'),
            ])
            ->where('ip', $this->ip)
            ->where('started_at', '>=', now()->subDays(7))
            ->first();

        return [
            'peticiones' => (int) ($fila->peticiones ?? 0),
            'errores' => (int) ($fila->errores ?? 0),
            'dominios' => (int) ($fila->dominios ?? 0),
            // Los dominios concretos, hasta cinco: es lo que dice si bloquear afecta a
            // uno o a varios clientes.
            'hosts' => ApiRequestLog::query()
                ->where('ip', $this->ip)
                ->where('started_at', '>=', now()->subDays(7))
                ->whereNotNull('host')
                ->distinct()
                ->limit(5)
                ->pluck('host')
                ->all(),
        ];
    }

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

        $datos = $this->validate([
            'ip' => ['required', 'ip'],
            // Obligatorio y con un mínimo: un bloqueo manual no caduca solo, así que el
            // motivo es lo único que permite decidir después si sobra.
            'motivo' => ['required', 'string', 'min:5', 'max:255'],
            'modo' => ['required', 'in:cortar,observar'],
        ], [
            'motivo.required' => 'Escribe el motivo: es lo que se leerá cuando haya que decidir si el bloqueo sobra.',
            'motivo.min' => 'Un motivo de menos de 5 caracteres no explica nada.',
            'ip.ip' => 'Eso no es una dirección IP válida.',
        ], ['ip' => 'IP', 'motivo' => 'motivo', 'modo' => 'modo']);

        // Si ya está bloqueada a mano, no se duplica.
        $existente = ApiBlock::blocked()
            ->where('subject_type', ApiBlock::SUBJECT_IP)
            ->where('reason', ApiBlock::REASON_MANUAL)
            ->where('ip', $datos['ip'])
            ->first();

        if ($existente) {
            session()->flash('warning', 'La IP ' . $datos['ip'] . ' ya estaba bloqueada a mano desde el '
                . $existente->first_blocked_at->format('d/m/Y H:i') . '.');

            $this->closeModalWithEvents(['apiBlocksChanged']);

            return;
        }

        $corta = $datos['modo'] === 'cortar';

        ApiBlock::create([
            'subject_type' => ApiBlock::SUBJECT_IP,
            'ip' => $datos['ip'],
            'reason' => ApiBlock::REASON_MANUAL,
            'state' => ApiBlock::STATE_BLOCKED,
            // **El modo lo elige quien bloquea**, y un bloqueo manual que corta lo hace
            // también cuando los límites automáticos están en observación: lo ha decidido
            // una persona mirando los datos, no un umbral puesto a ojo.
            'enforced' => $corta,
            'first_blocked_at' => now(),
            'last_blocked_at' => now(),
            'hits' => 0,
            'limit_value' => 0,
            'cleared_note' => $datos['motivo'],
            // No se avisa por correo: lo acaba de hacer una persona, ya lo sabe.
            'notified_at' => now(),
        ]);

        MonitoringLog::db(
            'warning',
            '16011',
            'IP bloqueada a mano: ' . $datos['ip']
            . ($corta ? ' (cortando)' : ' (solo observación, no corta)')
            . ' — ' . $datos['motivo']
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'ApiBlock',
            $datos['ip']
        );

        // La consulta del middleware va cacheada 30 s: sin esto, el bloqueo tardaría
        // media hora en notarse en el peor caso.
        ApiBlock::olvidarCacheDe($datos['ip']);

        session()->flash('success', $corta
            ? 'IP ' . $datos['ip'] . ' bloqueada. Sus peticiones a la API recibirán un 403 hasta '
              . 'que se marque como revisada.'
            : 'IP ' . $datos['ip'] . ' en observación. NO se corta nada: se registra para ver '
              . 'el efecto que tendría el corte.');

        // Clave numérica: el paquete hace `[$event, $params] = $event` cuando el valor es
        // un array, así que `['evento' => []]` revienta con "Undefined array key 0".
        $this->closeModalWithEvents(['apiBlocksChanged']);
    }

    public function render()
    {
        return view('livewire.monitoring.ip-block-modal', [
            'seEscribeLaIp' => $this->seEscribeLaIp(),
            'trafico' => $this->trafico(),
        ]);
    }
}
