<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;

/**
 * Como el cast `encrypted` de Laravel, pero **un valor que no se puede descifrar
 * devuelve null en vez de tumbar la página**.
 *
 * **Por qué hace falta.** El pipeline de despliegue de pre ejecuta
 * `php artisan key:generate` en cada pasada. Si esa clave se regenera, **todo lo cifrado
 * con la anterior queda ilegible**, y el cast estándar lanza `DecryptException` al
 * leerlo. `environments.moodletoken` se lee en el render de **cada tarjeta** del listado
 * de entornos, así que eso no daría un fallo discreto: daría un **error 500 en la
 * pantalla principal**, y sin ninguna pista de que la causa es la clave.
 *
 * Con esto, una rotación de clave degrada a «este entorno no tiene token»: la pantalla
 * sigue funcionando, «Sincronizar ahora» desaparece y quien lo necesite vuelve a pegar el
 * token. Que es exactamente lo que hay que hacer, porque el valor viejo **no se puede
 * recuperar**.
 *
 * **No se traga el problema en silencio**: deja un aviso en el log (con freno de una
 * hora, porque si no un listado de 200 entornos escribiría 200 líneas iguales), que es
 * lo que permite atar el síntoma a la causa.
 *
 * Ver `deploy-y-cicd.md`.
 */
class SafeEncrypted implements CastsAttributes
{
    public function get(Model $model, string $key, mixed $value, array $attributes): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        try {
            return Crypt::decryptString($value);
        } catch (DecryptException) {
            $this->avisar($model, $key);

            return null;
        }
    }

    public function set(Model $model, string $key, mixed $value, array $attributes): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        return Crypt::encryptString((string) $value);
    }

    /**
     * Un aviso por modelo y campo cada hora.
     *
     * El freno es por el caso que lo motiva: si la clave ha cambiado, **fallan todas las
     * filas a la vez**, y un log con una línea por fila esconde el mensaje en lugar de
     * enseñarlo.
     */
    private function avisar(Model $model, string $key): void
    {
        $freno = 'cast:indescifrable:' . $model->getTable() . ':' . $key;

        if (!Cache::add($freno, true, 3600)) {
            return;
        }

        Log::warning('Valor cifrado que no se puede descifrar: se devuelve null.', [
            'tabla' => $model->getTable(),
            'campo' => $key,
            'causa_probable' => 'APP_KEY distinta de la que cifró el valor '
                . '(¿un `php artisan key:generate` en el despliegue?)',
            'consecuencia' => 'El valor no es recuperable: hay que volver a introducirlo.',
        ]);
    }
}
