<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

/**
 * Bloqueos activos de la API: quién está bloqueado AHORA, desde cuándo y por qué.
 *
 * **Por qué hace falta otra tabla teniendo `api_rate_events`.** Esa guarda ventanas de
 * un minuto: sirve para ver el histórico y para el Dashboard, pero no responde a "¿esta
 * IP está bloqueada?" ni a "¿es la primera vez?". Y sin eso no se puede avisar por
 * correo **una sola vez**, que es lo pedido: hoy el aviso o no salía —venía desactivado
 * por defecto— o habría salido una vez por hora mientras durase el problema.
 *
 * Aquí hay **una fila por sujeto y motivo**, con estado:
 *
 *   bloqueado    se ha pasado del límite y sigue considerado bloqueado
 *   liberado     alguien lo ha revisado y lo ha marcado como resuelto
 *
 * El correo sale en la **transición** a bloqueado: al crear la fila, o cuando una fila
 * liberada vuelve a bloquearse. Los rechazos siguientes solo suben el contador.
 *
 * El sujeto es una IP o un entorno, porque los dos límites de la API cuentan por cosas
 * distintas: el de `ThrottleApiRequests` por IP, y el de la licencia por token —y de un
 * token se llega a su entorno, que es lo que un humano reconoce—.
 */
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('api_blocks', function (Blueprint $table) {
            $table->id();

            // Qué se ha bloqueado. Se guardan los dos identificadores porque una IP
            // puede no corresponder a ningún entorno conocido —el caso del sitio en
            // bucle con un token inexistente— y un entorno puede llamar desde varias.
            $table->string('subject_type', 20);
            $table->string('ip', 45)->nullable();
            $table->foreignId('environment_id')->nullable()
                ->constrained('environments')->nullOnDelete()->cascadeOnUpdate();
            $table->foreignId('license_token_id')->nullable()
                ->constrained('license_tokens')->nullOnDelete()->cascadeOnUpdate();

            // Motivo: fallos de autenticación, techo de peticiones, o límite de la
            // licencia. Mismos valores que `api_rate_events.limit_kind`.
            $table->string('reason', 20);

            $table->string('state', 20)->default('blocked');

            $table->dateTime('first_blocked_at');
            $table->dateTime('last_blocked_at');
            $table->unsignedInteger('hits')->default(1);
            $table->unsignedInteger('limit_value')->default(0);

            // Cuándo se avisó por correo de ESTE bloqueo. Nulo = pendiente de avisar,
            // que es lo que permite que el envío sea idempotente si algo falla.
            $table->dateTime('notified_at')->nullable();

            $table->dateTime('cleared_at')->nullable();
            $table->foreignId('cleared_by')->nullable()
                ->constrained('users')->nullOnDelete()->cascadeOnUpdate();
            $table->string('cleared_note')->nullable();

            // Contexto de la última petición rechazada, para poder diagnosticar sin
            // salir de la pantalla.
            $table->string('host')->nullable();
            $table->string('action', 50)->nullable();
            $table->string('plugin', 100)->nullable();
            $table->string('user_agent')->nullable();

            $table->timestamps();

            // Un sujeto y un motivo tienen una sola fila viva. Al liberarlo NO se borra
            // —el histórico importa— así que la unicidad incluye el estado... y eso no
            // se puede expresar en un índice. Se resuelve con la clave de abajo más la
            // lógica del modelo: se busca la fila `blocked`, y si no hay, se crea.
            $table->index(['subject_type', 'ip', 'reason']);
            $table->index(['state', 'first_blocked_at']);
            $table->index('environment_id');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('api_blocks');
    }
};
