<?php

namespace App\Console\Commands;

use App\Models\ApiRequestLog;
use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\ApiRateEvent;
use App\Services\Api\Severidad;
use App\Services\System\Settings;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

/**
 * Tráfico de mentira para ver funcionar la pantalla de Monitorización.
 *
 * **Por qué hace falta.** «¿Está pasando algo raro?» solo se puede evaluar viéndolo con
 * datos: con la tabla vacía no se sabe si el orden de las columnas ayuda, si el veredicto
 * de arriba dice lo que tiene que decir, ni si los enlaces llevan a donde prometen. Y
 * esperar a que pase de verdad significa evaluarlo el peor día.
 *
 * **Lo que genera, y por qué esos casos y no otros.** Cada IP de la lista reproduce una
 * situación que en la pantalla se lee distinto, incluidas las tres que confunden:
 *
 * 1. **Volumen alto y sin errores** — el caso normal que no hay que tocar. Si la pantalla
 *    lo pinta como alarma, la pantalla está mal.
 * 2. **Volumen bajo y todo errores** — el que sí importa, y el que se pierde si se ordena
 *    solo por volumen.
 * 3. **Un sujeto cortado sin ninguna petición registrada** — el que hace dudar de si el
 *    panel perdió datos: mientras el corte está activo se rechazan antes de registrarse
 *    (MGR-008), así que el enlace «Peticiones» no tiene nada que enseñar.
 * 4. **Una IP con muchos dominios** — un hosting compartido, donde cortar toca a varios
 *    clientes a la vez.
 * 5. **Ventanas que cruzaron el margen sin llegar al tope** — el aviso anticipado.
 *
 * **No se ejecuta en producción.** Escribe en las tablas de las que sale la monitorización
 * real, así que en producción falsearía justo lo que se mira para decidir. Se puede forzar,
 * pero hay que escribirlo.
 */
class SeedTrafficDemo extends Command
{
    protected $signature = 'monitoring:demo
                            {--limpiar : Borra lo que generó este comando y no crea nada}
                            {--force : Deja ejecutarlo en producción}';

    protected $description = 'Genera tráfico de prueba para ver funcionar Monitorización → Tráfico y cortes';

    /**
     * La marca que distingue lo de mentira de lo de verdad.
     *
     * **Va en el user-agent y no en una columna nueva**: es lo único que se puede añadir sin
     * tocar el esquema, se ve en pantalla —que es lo que se quiere, que nadie confunda estos
     * datos con tráfico real— y permite borrarlos después con un `where`.
     */
    public const MARCA = 'tresipunt-demo';

    /** Las IPs que usa, para poder limpiarlas sin tocar nada más. */
    private const IPS = [
        '203.0.113.10',
        '203.0.113.20',
        '203.0.113.30',
        '203.0.113.40',
        '203.0.113.50',
    ];

    public function handle(Settings $ajustes): int
    {
        if (app()->isProduction() && ! $this->option('force')) {
            $this->components->error(
                'Esto escribe en las tablas de la monitorización real. En producción falsearía '
                . 'lo que se mira para decidir. Si de verdad hace falta, --force.'
            );

            return self::FAILURE;
        }

        if ($this->option('limpiar')) {
            return $this->limpiar();
        }

        $umbrales = $ajustes->umbralesDeLaApi();

        $this->peticiones();
        $this->ventanas($umbrales);
        $this->episodios($umbrales);

        $this->olvidarCaches();

        $this->components->info('Listo. Monitorización → Tráfico y cortes.');
        $this->components->bulletList([
            '203.0.113.10 — 800 peticiones, ninguna con error: el caso normal',
            '203.0.113.20 — 40 peticiones y 38 errores: poco volumen, mucho ruido',
            '203.0.113.30 — cortada y sin ninguna petición registrada',
            '203.0.113.40 — 12 dominios distintos: cortarla toca a doce clientes',
            '203.0.113.50 — ventanas que cruzaron el margen sin llegar al tope',
        ]);
        $this->newLine();
        $this->components->warn('Para borrarlo: php artisan monitoring:demo --limpiar');

        return self::SUCCESS;
    }

    /**
     * Las peticiones del registro.
     *
     * Se insertan por lotes porque son unas mil: mil `create()` son mil consultas y el
     * comando tarda más que la pantalla que viene a probar.
     */
    private function peticiones(): void
    {
        $filas = [];

        // 1. Volumen alto y limpio.
        $filas = array_merge($filas, $this->lote('203.0.113.10', 800, 0, [
            'demo-uno.example',
        ]));

        // 2. Poco volumen y casi todo errores.
        $filas = array_merge($filas, $this->lote('203.0.113.20', 40, 38, [
            'demo-dos.example',
        ]));

        // 4. Un hosting compartido: doce dominios detrás de la misma IP.
        $dominios = [];

        for ($i = 1; $i <= 12; $i++) {
            $dominios[] = 'cliente-' . $i . '.hosting.example';
        }

        $filas = array_merge($filas, $this->lote('203.0.113.40', 240, 6, $dominios));

        // 5. La del margen: tiene tráfico, no llega al tope.
        $filas = array_merge($filas, $this->lote('203.0.113.50', 150, 2, [
            'demo-cinco.example',
        ]));

        // La 203.0.113.30 no lleva ninguna a propósito: es el sujeto cortado cuyas
        // peticiones se rechazaron antes de registrarse.

        foreach (array_chunk($filas, 200) as $trozo) {
            ApiRequestLog::insert($trozo);
        }

        $this->components->twoColumnDetail('Peticiones creadas', (string) count($filas));
    }

    /**
     * Un lote de peticiones de una IP, repartidas por las últimas horas.
     *
     * @param  array<int, string>  $dominios
     * @return array<int, array<string, mixed>>
     */
    private function lote(string $ip, int $cuantas, int $conError, array $dominios): array
    {
        $filas = [];
        $acciones = ['scss-cdn', 'setup', 'licence', 'features'];

        for ($i = 0; $i < $cuantas; $i++) {
            $esError = $i < $conError;
            // Repartidas por las últimas 24 h, no todas en el mismo segundo: la pantalla
            // agrupa por ventanas de un minuto y todas juntas darían una sola.
            $cuando = now()->subMinutes(random_int(1, 24 * 60));

            $filas[] = [
                'request_uuid' => (string) Str::uuid(),
                'method' => 'POST',
                'path' => 'api/v1',
                // No admite nulo: toda petición de la API llega con su token.
                'auth_type' => 'bearer',
                'ip' => $ip,
                'user_agent' => self::MARCA . '/1.0',
                'action' => $acciones[array_rand($acciones)],
                'host' => $dominios[array_rand($dominios)],
                'version' => '2026010100',
                'http_status' => $esError ? 403 : 200,
                'severity' => $esError ? Severidad::ERROR : Severidad::OK,
                'success' => ! $esError,
                'error_code' => $esError ? 2001 : null,
                'error' => $esError ? 'Licence does not include this plugin' : null,
                'response_size' => $esError ? 120 : random_int(2000, 60000),
                'duration_ms' => random_int(40, 800),
                'started_at' => $cuando,
                // Sin `created_at`/`updated_at`: `api_request_logs` no los tiene. La fila
                // **es** el registro de un momento, y para eso están `started_at` y
                // `ended_at`.
                'ended_at' => $cuando->copy()->addMilliseconds(random_int(40, 800)),
            ];
        }

        return $filas;
    }

    /**
     * Las ventanas del limitador: las que avisan y las que cortan.
     *
     * @param  array<string, mixed>  $umbrales
     */
    private function ventanas(array $umbrales): void
    {
        $tope = (int) $umbrales['requests'];
        $margen = (int) round($tope * (float) $umbrales['warning_ratio']);

        $filas = [];

        // Seis ventanas que cruzaron el margen sin llegar al tope: es el aviso anticipado,
        // y es la tabla que contesta «¿quién va a acabar cortado si sigue así?».
        for ($i = 1; $i <= 6; $i++) {
            $cuando = now()->subHours($i * 3)->startOfMinute();

            $filas[] = [
                'ip' => '203.0.113.50',
                'window_started_at' => $cuando,
                'limit_kind' => ApiRateEvent::LIMIT_REQUESTS,
                'kind' => ApiRateEvent::KIND_WARNING,
                'observed' => $margen + random_int(1, max(1, (int) (($tope - $margen) / 2))),
                'limit_value' => $tope,
                'rejected' => 0,
                'host' => 'demo-cinco.example',
                'action' => 'scss-cdn',
                'user_agent' => self::MARCA . '/1.0',
                'created_at' => $cuando,
                'updated_at' => $cuando,
            ];
        }

        // Y tres que sí cortaron, de la IP que no deja rastro en el registro.
        for ($i = 1; $i <= 3; $i++) {
            $cuando = now()->subHours($i)->startOfMinute();

            $filas[] = [
                'ip' => '203.0.113.30',
                'window_started_at' => $cuando,
                'limit_kind' => ApiRateEvent::LIMIT_FAILURES,
                'kind' => ApiRateEvent::KIND_BLOCKED,
                'observed' => (int) $umbrales['failures'] + random_int(5, 40),
                'limit_value' => (int) $umbrales['failures'],
                'rejected' => random_int(40, 300),
                'host' => null,
                'action' => 'licence',
                'user_agent' => self::MARCA . '/1.0',
                'created_at' => $cuando,
                'updated_at' => $cuando,
            ];
        }

        ApiRateEvent::insert($filas);

        $this->components->twoColumnDetail('Ventanas del limitador', (string) count($filas));
    }

    /**
     * Los episodios de corte: uno activo, uno ya revisado.
     *
     * @param  array<string, mixed>  $umbrales
     */
    private function episodios(array $umbrales): void
    {
        // Activo y sin revisar: es lo que la pantalla pone arriba del todo.
        ApiBlock::create([
            'subject_type' => ApiBlock::SUBJECT_IP,
            'ip' => '203.0.113.30',
            'reason' => ApiBlock::REASON_FAILURES,
            'state' => ApiBlock::STATE_BLOCKED,
            'enforced' => false,
            'first_blocked_at' => now()->subHours(4),
            'last_blocked_at' => now()->subMinutes(20),
            'hits' => 720,
            'limit_value' => (int) $umbrales['failures'],
            'action' => 'licence',
            'user_agent' => self::MARCA . '/1.0',
        ]);

        // Ya revisado: es el que se puede borrar, y sirve para probar que el botón de
        // borrar del grupo tiene algo que borrar.
        ApiBlock::create([
            'subject_type' => ApiBlock::SUBJECT_IP,
            'ip' => '203.0.113.20',
            'reason' => ApiBlock::REASON_FAILURES,
            'state' => ApiBlock::STATE_CLEARED,
            'enforced' => false,
            'first_blocked_at' => now()->subDays(2),
            'last_blocked_at' => now()->subDays(2)->addMinutes(3),
            'hits' => 38,
            'limit_value' => (int) $umbrales['failures'],
            'cleared_at' => now()->subDay(),
            'cleared_note' => 'Demo: revisado para poder probar el borrado.',
            'action' => 'licence',
            'user_agent' => self::MARCA . '/1.0',
        ]);

        $this->components->twoColumnDetail('Episodios de corte', '2');
    }

    private function limpiar(): int
    {
        $peticiones = ApiRequestLog::where('user_agent', 'like', self::MARCA . '%')->forceDelete();
        $ventanas = ApiRateEvent::whereIn('ip', self::IPS)->delete();
        $episodios = ApiBlock::whereIn('ip', self::IPS)->delete();

        $this->olvidarCaches();

        $this->components->twoColumnDetail('Peticiones borradas', (string) $peticiones);
        $this->components->twoColumnDetail('Ventanas borradas', (string) $ventanas);
        $this->components->twoColumnDetail('Episodios borrados', (string) $episodios);

        return self::SUCCESS;
    }

    /**
     * Las mismas cachés que tira la pantalla al tocar datos.
     *
     * Sin esto el comando termina, se abre Monitorización y no se ve nada durante un minuto
     * — que es justo la confusión que este comando existe para quitar.
     */
    private function olvidarCaches(): void
    {
        foreach (['monitoring:trafico:resumen', 'monitoring:estado', 'monitoring:portada'] as $clave) {
            Cache::forget($clave);
        }

        foreach ([1, 7, 30] as $dias) {
            foreach (['0', '1'] as $soloErrores) {
                Cache::forget('monitoring:ips:' . $dias . ':' . $soloErrores);
            }
        }
    }
}
