<?php

namespace App\Livewire\LicenseTokens;

use App\Models\Monitoring\ApiRateEvent;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Models\Products\LicenseToken;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use LivewireUI\Modal\ModalComponent;

/**
 * Cambiar el límite de peticiones de **una** licencia, desde su ficha.
 *
 * **Por qué aquí y no solo en «Editar».** El límite se puede cambiar en el formulario de
 * edición, pero ahí es un campo más entre cliente, fechas, modo, metadatos y
 * observaciones: se toca sin ver nada de lo que importa para decidir el número. Y lo que
 * importa está justo en la ficha, al lado —**cuánto consume ahora mismo** y **cuántas
 * veces ha cortado**—, que es el mismo contador que usa el middleware.
 *
 * Bajar este número **corta servicio a un cliente**: al superarlo, su Moodle deja de
 * recibir licencias y contenido hasta que baje del tope. Así que la modal enseña, antes de
 * guardar, el consumo del minuto en curso y el pico de la última semana, y avisa cuando el
 * número que se está poniendo ya está por debajo de lo que ese cliente gasta.
 *
 * No sustituye al formulario: es el atajo para la única cosa de esta ficha que se cambia
 * mirando los datos de esta ficha.
 */
class RateLimitModal extends ModalComponent
{
    public int $licenseTokenId;

    /**
     * El tope, como texto.
     *
     * Texto y no `?int` a propósito: el campo se vacía para volver al techo por defecto, y
     * con un `?int` un vacío llega como `null` igual que un cero, sin poder distinguir
     * «lo he borrado» de «he escrito 0».
     */
    public string $tope = '';

    public function mount(int $licenseTokenId): void
    {
        // El permiso de la ruta no se reaplica en `/livewire/update` (MGR-005), y esta
        // modal escribe.
        $this->authorize('admin.license-tokens.edit');

        $licencia = LicenseToken::findOrFail($licenseTokenId);

        $this->licenseTokenId = $licencia->id;
        $this->tope = $licencia->rate_limit_per_minute !== null
            ? (string) $licencia->rate_limit_per_minute
            : '';
    }

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

    public function licencia(): LicenseToken
    {
        return LicenseToken::findOrFail($this->licenseTokenId);
    }

    /**
     * El techo que aplica el middleware cuando la licencia no tiene tope propio.
     *
     * Diez mil por minuto: en la práctica, no tener límite. Está aquí y no en `config`
     * porque es el valor del middleware y **tiene que decir lo mismo que la ficha**; si
     * algún día se mueve a la configuración, se mueve en los dos sitios.
     */
    public const TECHO_POR_DEFECTO = 10000;

    /**
     * Lo que este cliente está gastando de verdad.
     *
     * Del mismo contador que usa el middleware, así que es el consumo real. Es del minuto
     * **en curso**: al cambiar de minuto vuelve a cero, y eso hay que decirlo o el número
     * desconcierta.
     *
     * @return array{ahora:int, pico:int, cortes:int}
     */
    public function consumo(): array
    {
        $licencia = $this->licencia();

        return [
            'ahora' => RateLimiter::attempts('rate_limit:token:' . $licencia->token),
            // El pico de la semana: es el número contra el que hay que comparar el tope,
            // no el del minuto actual —que puede ser cero justo cuando lo miras—.
            //
            // `observed` son las peticiones que se contaron en esa ventana de un minuto,
            // pasadas o rechazadas. Es el gasto real del cliente; `rejected` es solo la
            // parte que se le negó, y comparar el tope contra eso diría que un cliente al
            // que nunca se ha cortado no gasta nada.
            'pico' => (int) ApiRateEvent::where('license_token_id', $licencia->id)
                ->where('limit_kind', ApiRateEvent::LIMIT_TOKEN)
                ->where('window_started_at', '>=', now()->subWeek())
                ->max('observed'),
            'cortes' => (int) ApiRateEvent::where('license_token_id', $licencia->id)
                ->where('limit_kind', ApiRateEvent::LIMIT_TOKEN)
                ->where('window_started_at', '>=', now()->subWeek())
                ->sum('rejected'),
        ];
    }

    /**
     * Qué va a pasar cuando se guarde.
     *
     * **No es un «¿estás seguro?».** Lo que hace que alguien lea un aviso es que le diga
     * qué va a pasar y a quién. Aquí lo grave es poner un tope por debajo de lo que el
     * cliente ya gasta: eso empieza a devolverle 429 en cuanto se guarde, y su sitio deja
     * de recibir licencias y contenido.
     *
     * @return array{aviso:?string, grave:bool}
     */
    public function consecuencia(): array
    {
        $nuevo = trim($this->tope);
        $consumo = $this->consumo();
        $referencia = max($consumo['ahora'], $consumo['pico']);

        if ($nuevo === '') {
            return [
                'aviso' => 'Esta licencia dejará de tener tope propio y se le aplicará el techo por '
                    . 'defecto de ' . number_format(self::TECHO_POR_DEFECTO, 0, ',', '.')
                    . ' peticiones por minuto, que en la práctica es no tener límite.',
                'grave' => false,
            ];
        }

        $valor = (int) $nuevo;

        // El caso que corta: el tope queda por debajo de lo que ese cliente ya gasta.
        if ($referencia > 0 && $valor <= $referencia) {
            return [
                'aviso' => 'Este cliente ha llegado a ' . number_format($referencia, 0, ',', '.')
                    . ' peticiones en un minuto esta semana. Con un tope de '
                    . number_format($valor, 0, ',', '.') . ' empezará a recibir 429 en cuanto se '
                    . 'guarde: su Moodle deja de recibir licencias y contenido hasta que baje del tope.',
                'grave' => true,
            ];
        }

        if ($consumo['cortes'] > 0) {
            return [
                'aviso' => 'A esta licencia ya se le han rechazado '
                    . number_format($consumo['cortes'], 0, ',', '.') . ' peticiones esta semana por '
                    . 'este límite. Súbelo si el corte no era intencionado.',
                'grave' => false,
            ];
        }

        return [
            'aviso' => 'Desde que se guarde, este cliente podrá hacer '
                . number_format($valor, 0, ',', '.') . ' peticiones por minuto. El contador se '
                . 'reinicia cada minuto, así que no es una cuota total.',
            'grave' => false,
        ];
    }

    public function guardar(): void
    {
        $this->authorize('admin.license-tokens.edit');

        // Vacío significa «sin tope propio», y hay que poder escribirlo: la validación
        // acepta la cadena vacía y guarda null.
        $this->validate([
            'tope' => 'nullable|integer|min:1|max:1000000',
        ], [
            'tope.integer' => 'El tope es un número de peticiones por minuto.',
            'tope.min' => 'Un tope de 0 dejaría al cliente sin poder llamar. Déjalo vacío para quitar el tope propio.',
            'tope.max' => 'Un tope tan alto es lo mismo que no tenerlo: déjalo vacío.',
        ]);

        $licencia = $this->licencia();
        $antes = $licencia->rate_limit_per_minute;
        $nuevo = trim($this->tope) === '' ? null : (int) $this->tope;

        if ($antes === $nuevo) {
            // Nada que guardar: no se escribe en el log ni se dice que se ha cambiado algo
            // que sigue igual.
            $this->closeModal();

            return;
        }

        $licencia->update(['rate_limit_per_minute' => $nuevo]);

        // **Queda el antes y el después.** Un «límite cambiado» sin los dos números no
        // sirve para lo único que se consulta este log: por qué un cliente empezó a
        // recibir 429 un martes por la tarde.
        MonitoringLog::db(
            'warning',
            '16030',
            'Límite de peticiones de la licencia ' . $licencia->token . ': '
            . ($antes === null ? 'sin tope propio' : $antes . '/min')
            . ' → '
            . ($nuevo === null ? 'sin tope propio' : $nuevo . '/min')
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'LicenseToken',
            (string) $licencia->id
        );

        session()->flash('success', $nuevo === null
            ? 'Esta licencia ya no tiene tope propio: se le aplica el techo por defecto.'
            : 'Límite guardado: ' . number_format($nuevo, 0, ',', '.') . ' peticiones por minuto.');

        // 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(['rateLimitChanged']);
    }

    public function render()
    {
        return view('livewire.license-tokens.rate-limit-modal', [
            'licencia' => $this->licencia(),
            'consumo' => $this->consumo(),
            'consecuencia' => $this->consecuencia(),
            'techoPorDefecto' => self::TECHO_POR_DEFECTO,
        ]);
    }
}
