<?php

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

/**
 * Cifra `environments.moodletoken` en reposo.
 *
 * Ese campo guarda el **token de servicios web del Moodle del cliente**: una
 * credencial de administración de un sistema ajeno. Llega dentro del `sync` (en
 * `site.token`) y hasta ahora se guardaba en claro, cuando además no se usaba para
 * nada. Desde la 2.0.0 sí se usa —el botón "Sincronizar ahora" llama al Moodle con
 * ella—, así que una copia de la base de datos, un volcado de soporte o una consulta
 * de más entrega el control de los Moodles de todos los clientes.
 *
 * Ver known-issues MGR-038.
 *
 * **Primero se ensancha la columna y después se cifra**, y el orden no es opcional:
 * era `varchar(255)` y el cifrado de un token de 32 caracteres ocupa **256**. Cifrar
 * sin ensanchar da error en MariaDB con modo estricto, y trunca sin avisar sin él —o
 * sea, tokens irrecuperables—.
 *
 * **Sobre la reversibilidad y el APP_KEY**: si se pierde o se rota el `APP_KEY`, estos
 * valores no se pueden descifrar. No es grave en este caso concreto y conviene saber
 * por qué: **cada Moodle reenvía su token en cada sincronización**, así que el dato se
 * regenera solo. Es una credencial replicable, no un secreto único. Lo que sí hay que
 * evitar es rotar el `APP_KEY` y dar por hecho que el botón de sincronizar sigue
 * funcionando hasta que los sitios vuelvan a sincronizar.
 */
return new class extends Migration
{
    public function up(): void
    {
        Schema::table('environments', function (Blueprint $table) {
            // `text` y no `varchar(512)`: si mañana cambia el cifrador o el tamaño del
            // token, no hay que volver a pasar por aquí.
            $table->text('moodletoken')->nullable()->change();
        });

        $this->recorrerTokens(function (string $valor): ?string {
            // Idempotente: si ya está cifrado —porque la migración se repita, o porque
            // el valor lo escribió el modelo con el cast ya puesto— se deja como está.
            if ($this->yaEstaCifrado($valor)) {
                return null;
            }

            return Crypt::encryptString($valor);
        });
    }

    public function down(): void
    {
        // Se descifra ANTES de estrechar la columna, o los valores no cabrían.
        $this->recorrerTokens(function (string $valor): ?string {
            if (!$this->yaEstaCifrado($valor)) {
                return null;
            }

            try {
                return Crypt::decryptString($valor);
            } catch (\Throwable) {
                // Sin poder descifrarlo, dejarlo cifrado sería peor que vaciarlo: el
                // sitio reenvía su token en la próxima sincronización.
                return '';
            }
        });

        Schema::table('environments', function (Blueprint $table) {
            $table->string('moodletoken')->nullable()->change();
        });
    }

    /**
     * Aplica una transformación a cada token no vacío. Si el callback devuelve `null`,
     * esa fila se deja intacta.
     *
     * Se usa el query builder y no el modelo a propósito: el modelo tiene el cast
     * `encrypted` y cifraría o descifraría por su cuenta, dejando el valor de la
     * migración cifrado dos veces.
     */
    private function recorrerTokens(callable $transformar): void
    {
        DB::table('environments')
            ->whereNotNull('moodletoken')
            ->where('moodletoken', '<>', '')
            ->select('id', 'moodletoken')
            ->orderBy('id')
            ->chunk(200, function ($filas) use ($transformar) {
                foreach ($filas as $fila) {
                    $nuevo = $transformar((string) $fila->moodletoken);

                    if ($nuevo === null) {
                        continue;
                    }

                    DB::table('environments')
                        ->where('id', $fila->id)
                        ->update(['moodletoken' => $nuevo]);
                }
            });
    }

    /**
     * ¿Este valor ya está cifrado por Laravel?
     *
     * Se comprueba intentando descifrarlo, que es la única forma fiable: el formato es
     * un JSON en base64 con `iv`, `value` y `mac`, y un token de Moodle en claro (32
     * caracteres hexadecimales) nunca lo parece.
     */
    private function yaEstaCifrado(string $valor): bool
    {
        try {
            Crypt::decryptString($valor);

            return true;
        } catch (\Throwable) {
            return false;
        }
    }
};
