<?php

namespace App\Livewire\Configurations\Manager;

use App\Models\Monitoring\ApiBlock;
use App\Models\Monitoring\Log as MonitoringLog;
use App\Services\System\Settings;
use Illuminate\Support\Facades\Auth;
use LivewireUI\Modal\ModalComponent;

/**
 * Editar **un** ajuste — desde `Configuración.dc.html`.
 *
 * **Por qué uno y no una pantalla de formularios.** La pantalla de Ajustes eran 802
 * líneas de blade con cuatro pestañas propias —pestañas dentro de pestañas— y seis
 * botones de guardar. Con eso, guardar el destinatario de los avisos de bloqueo y bajar
 * un tope de la API se parecían: mismo formulario, mismo botón, mismo aspecto. Y no son
 * lo mismo: lo primero no afecta a ningún cliente, y lo segundo puede empezar a devolver
 * 429 a sitios que hasta ese momento pasaban.
 *
 * Editando de uno en uno, **cada cambio se puede explicar antes de hacerlo**: la modal
 * dice qué vale ahora, qué va a valer, y qué pasa cuando se guarde. Y ya no se pisa lo
 * de otro por guardar lo tuyo.
 *
 * **Las claves salen de `Settings::CATALOGO`, no del navegador.** `$clave` es una
 * propiedad pública y llega en el snapshot, así que se comprueba contra el catálogo en el
 * `mount()` **y** al guardar (MGR-018): sin eso, cualquiera podría escribir en
 * `configs` una fila arbitraria del ámbito `manager`.
 */
class SettingModal extends ModalComponent
{
    public string $clave = '';

    /**
     * La unidad de este ajuste, ya en singular o plural según lo que se esté escribiendo.
     *
     * Se pinta dentro del campo. Ver {@see \App\Services\System\Settings::unidadDe()}:
     * el catálogo la declara junto al ajuste, que es donde no se puede desincronizar de
     * su descripción.
     */
    public function unidad(): string
    {
        return app(\App\Services\System\Settings::class)->unidadDe($this->clave, $this->valor);
    }

    /** Lo que se va a guardar. Siempre texto: la tabla `configs` guarda cadenas. */
    public string $valor = '';

    /** Para los de tipo `bool`, el radio elegido. */
    public bool $activo = false;

    /** Para el de tipo `modo`: `observar` o `cortar`. */
    public string $modo = 'observar';

    public function mount(string $clave): void
    {
        // El permiso de la ruta no se reaplica en /livewire/update. Ver MGR-005.
        $this->authorize('admin.configs.edit');

        $ajustes = app(Settings::class);

        // Una clave que no está declarada no se edita: ver el docblock.
        abort_unless($ajustes->estaEnElCatalogo($clave), 404);

        $this->clave = $clave;

        $ficha = $ajustes->fichaDe($clave);

        $this->valor = $ficha['heredado'] ? '0' : (string) ($ficha['crudo'] ?? '');
        $this->activo = $ajustes->activo($clave);
        $this->modo = $ajustes->losLimitesCortan() ? 'cortar' : 'observar';
    }

    public static function modalMaxWidth(): string
    {
        return 'lg';
    }

    /**
     * Lo que hay que saber antes de guardar, por tipo.
     *
     * **No es un «¿estás seguro?».** Lo que hace que alguien lea un aviso es que le diga
     * qué va a pasar y a cuántos: los casos marcados como consecuencia grave son los que
     * cortan servicio o dejan de avisar en silencio.
     *
     * @return array{aviso: ?string, grave: bool}
     */
    public function consecuencia(): array
    {
        $ajustes = app(Settings::class);
        $meta = $ajustes->metaDe($this->clave);

        // ---- El modo de los límites: el único que corta servicio ----
        if ($meta['tipo'] === 'modo') {
            if ($this->modo !== 'cortar') {
                return [
                    'aviso' => 'Los límites dejarán de cortar: se seguirá registrando y avisando, '
                        . 'pero ninguna petición será rechazada. Es el modo seguro.',
                    'grave' => false,
                ];
            }

            // La cuenta va antes de la confirmación: es la diferencia entre activar el
            // corte sin consecuencias y cortar a cinco sitios esta misma tarde.
            $observados = ApiBlock::blocked()->where('enforced', false)->count();

            return [
                'aviso' => 'Desde que guardes, el sitio que supere un tope deja de recibir licencias '
                    . 'y contenido hasta que baje de él. '
                    . ($observados > 0
                        ? 'Ahora mismo hay ' . $observados . ' caso(s) marcado(s) como «se habría '
                            . 'bloqueado»: son los que empezarían a cortarse.'
                        : 'Ahora mismo no hay ningún caso pasándose del límite.'),
                'grave' => true,
            ];
        }

        // ---- Los avisos: dejar de avisar no da ningún error ----
        if ($meta['tipo'] === 'bool' && !$this->activo) {
            return [
                'aviso' => 'Se deja de avisar sin ningún error visible: nadie recibirá estos '
                    . 'correos hasta volver a activarlo.',
                'grave' => true,
            ];
        }

        if ($meta['tipo'] === 'correos' && $ajustes->correosDeUnTexto($this->valor) === []) {
            return [
                'aviso' => 'Sin ningún destinatario no se envía nada, y no salta ningún error: '
                    . 'el aviso queda activado y no llega a nadie.',
                'grave' => true,
            ];
        }

        // ---- La retención: borra rastro de auditoría ----
        if ($this->clave === Settings::LOGS_RETENCION) {
            $dias = (int) $this->valor;

            if ($dias === 0) {
                return [
                    'aviso' => 'Con 0 no se borra nada nunca. La tabla de peticiones es la más '
                        . 'grande del sistema y crece sin freno hasta que el visor deja de abrirse.',
                    'grave' => true,
                ];
            }

            return [
                'aviso' => 'Cada noche a las 03:30 se borrará lo anterior a ' . $dias . ' días. '
                    . 'Lo que se va no vuelve.',
                'grave' => false,
            ];
        }

        return ['aviso' => null, 'grave' => false];
    }

    public function guardar(): void
    {
        $this->authorize('admin.configs.edit');

        $ajustes = app(Settings::class);

        // Se vuelve a comprobar: `$clave` viaja en el snapshot y llega del navegador.
        abort_unless($ajustes->estaEnElCatalogo($this->clave), 404);

        $meta = $ajustes->metaDe($this->clave);

        $guardado = match ($meta['tipo']) {
            'bool' => $this->guardarBool($ajustes),
            'modo' => $this->guardarModo($ajustes),
            'correos' => $this->guardarCorreos($ajustes),
            'dias' => $this->guardarDias($ajustes),
            default => $this->guardarEntero($ajustes, $meta),
        };

        if ($guardado === null) {
            // La validación ha fallado: la modal se queda abierta con el error puesto.
            return;
        }

        // **Queda escrito el valor efectivo, no lo que se escribió en el campo.** Un
        // «guardado 0» en el log no dice nada, y saber con qué números estaba corriendo
        // la API es justo lo que hace falta cuando alguien pregunta por qué se cortó.
        MonitoringLog::db(
            'warning',
            '16014',
            'Ajuste cambiado: ' . $meta['desc'] . ' (' . $this->clave . ') → ' . $guardado
            . ' (por ' . (Auth::user()?->name ?? 'desconocido') . ')',
            'Config',
            $this->clave
        );

        session()->flash('success', '«' . $meta['desc'] . '» guardado: ' . $guardado . '.');

        // Clave numérica: el paquete hace `[$event, $params] = $event` cuando el valor es
        // un array, así que `['evento' => []]` revienta con "Undefined array key 0".
        $this->closeModalWithEvents(['settingsChanged']);
    }

    private function guardarBool(Settings $ajustes): string
    {
        $ajustes->set($this->clave, $this->activo ? '1' : '0');

        return $this->activo ? 'activado' : 'desactivado';
    }

    private function guardarModo(Settings $ajustes): string
    {
        $corta = $this->modo === 'cortar';

        $ajustes->set(Settings::LIMITES_CORTAN, $corta ? '1' : '0');

        return $corta
            ? 'los límites CORTAN peticiones'
            : 'los límites solo observan, no se corta a nadie';
    }

    private function guardarCorreos(Settings $ajustes): ?string
    {
        // Se valida cada dirección por separado **diciendo cuál está mal**: con un `email`
        // a secas Laravel rechaza la lista entera sin decir la culpable, y con cuatro
        // destinatarios eso es un juego de adivinanzas.
        $this->validate([
            'valor' => ['nullable', 'string', 'max:500', function ($atributo, $valor, $fallo) {
                foreach (array_filter(array_map('trim', explode(',', (string) $valor))) as $correo) {
                    if (!filter_var($correo, FILTER_VALIDATE_EMAIL)) {
                        $fallo('No es una dirección válida: ' . $correo);
                    }
                }
            }],
        ], [], ['valor' => 'destinatarios']);

        $ajustes->set($this->clave, trim($this->valor));

        $correos = $ajustes->correos($this->clave);

        return $correos === [] ? 'sin destinatarios' : implode(', ', $correos);
    }

    private function guardarDias(Settings $ajustes): ?string
    {
        $this->validate([
            // Laxa a propósito —«30, 15, 7, 0» con espacios es lo natural de escribir— y
            // `diasDeAvisoDeSoportes()` la limpia al leerla: duplicados, negativos y el
            // tope de seis avisos.
            'valor' => ['nullable', 'string', 'max:100', 'regex:/^[0-9,\s]*$/'],
        ], [
            'valor.regex' => 'Los días son números separados por comas, por ejemplo: 30, 15, 7, 0',
        ], ['valor' => 'días de aviso']);

        $ajustes->set($this->clave, trim($this->valor));

        // Se devuelve la lista YA limpia: si alguien escribe «30,30,-5,900» tiene que ver
        // el resultado, no su texto.
        return implode(', ', $ajustes->diasDeAvisoDeSoportes()) . ' días antes';
    }

    private function guardarEntero(Settings $ajustes, array $meta): ?string
    {
        $this->validate([
            'valor' => ['required', 'integer', 'min:' . ($meta['min'] ?? 0), 'max:' . ($meta['max'] ?? 1000000)],
        ], [], ['valor' => 'valor']);

        $ajustes->set($this->clave, (string) (int) $this->valor);

        $ficha = $ajustes->fichaDe($this->clave);

        // Cuando queda heredado, se dice cuál se aplica de verdad: guardar un 0 y ver
        // «guardado: 0» no cuenta nada.
        return $ficha['heredado'] && $ficha['efectivo'] !== null
            ? '0, o sea heredado del servidor (' . $ficha['efectivo'] . ')'
            : $ficha['valor'];
    }

    public function render()
    {
        $ajustes = app(Settings::class);

        return view('livewire.configurations.manager.setting-modal', [
            'ficha' => $ajustes->fichaDe($this->clave),
            'meta' => $ajustes->metaDe($this->clave),
            'consecuencia' => $this->consecuencia(),
        ]);
    }
}
