<?php

use App\Services\Api\Motivo;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

/**
 * El motivo de cada rechazo, como dato.
 *
 * **Qué pregunta contesta que `severity` no contestaba.** `severity` dice «¿hay que
 * arreglarlo?» y con eso se construyó la norma de cero errores. La que se hace a diario es
 * otra: **¿por qué ha fallado?** — y eso solo estaba en el texto del mensaje.
 *
 * El resultado era que **ocho situaciones distintas compartían la etiqueta «No
 * contratado»**: el `403` del middleware es plano —`code: 403` para todo— y cubre seis
 * cosas (cliente de baja, licencia apagada, licencia sin empezar, licencia caducada,
 * entorno apagado y host que no es de esa licencia), más las dos de producto. Y no tienen
 * el mismo dueño: una la renueva Comercial, otra la reactivamos nosotros, y **una es un
 * alta mal hecha nuestra que estaba contando como respuesta correcta**.
 *
 * Con esta columna, «cuántos sitios están llamando con la licencia caducada» —la pregunta
 * de Comercial— pasa a ser un `group by`.
 *
 * ## Se rellena lo que ya estaba, y en SQL
 *
 * Es la misma decisión que en la columna `severity`, por el mismo motivo: el coste no
 * depende de cuántos rechazos haya, sino de **cuántas filas hay que mirar**, y
 * `api_request_logs` es la tabla que más crece del sistema. Recorrerla en PHP se quedó
 * colgado en pre la vez anterior. Aquí es un `update` por bloque de ids.
 *
 * Y se recorren **solo las filas de 400 o más**: las respondidas tienen motivo `ninguno`,
 * que es el valor por defecto de la columna, así que no hay que tocarlas.
 */
return new class extends Migration
{
    /**
     * Cuántos ids por `update`.
     *
     * Mismo tamaño que en la migración de `severity`: bloques por rango de id y no por
     * `limit`, para que cada sentencia use el índice de la clave primaria.
     */
    private const BLOQUE = 50000;

    public function up(): void
    {
        Schema::table('api_request_logs', function (Blueprint $table) {
            // 40 caracteres: el motivo más largo de la lista es `producto_no_contratado`
            // (22). El margen es para los que vengan.
            //
            // Con `ninguno` por defecto y no nulo: una fila sin clasificar y una fila
            // respondida son cosas distintas, y la columna no debería tener que
            // distinguirlas con un null que hay que interpretar.
            $table->string('reason', 40)->default(Motivo::NINGUNO)->after('severity');

            // El índice que hace útil la columna: la pregunta siempre es «en este rango,
            // agrupado por motivo», igual que con la severidad.
            $table->index(['started_at', 'reason'], 'api_logs_started_reason_idx');
        });

        $this->rellenar();
    }

    /**
     * Clasifica las filas que ya estaban.
     *
     * Se reescriben **todas** las de 400 o más, incluidas las que un intento anterior
     * hubiera clasificado: dejar la tabla con una parte escrita por un recorrido y otra por
     * el `case` es justo lo que no se quiere poder dudar cuando alguien mire el reparto.
     */
    private function rellenar(): void
    {
        $limites = DB::table('api_request_logs')
            ->where('http_status', '>=', 400)
            ->selectRaw('min(id) as desde, max(id) as hasta')
            ->first();

        if ($limites === null || $limites->desde === null) {
            return;
        }

        $sql = 'update api_request_logs set reason = ' . Motivo::comoCase()
            . ' where http_status >= 400 and id between ? and ?';

        for ($desde = (int) $limites->desde; $desde <= (int) $limites->hasta; $desde += self::BLOQUE) {
            DB::update($sql, [$desde, $desde + self::BLOQUE - 1]);
        }
    }

    public function down(): void
    {
        Schema::table('api_request_logs', function (Blueprint $table) {
            $table->dropIndex('api_logs_started_reason_idx');
            $table->dropColumn('reason');
        });
    }
};
