<?php

namespace App\Models\Monitoring;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Log extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'logs';

    protected $casts = ['created_at' => 'datetime'];

    protected $fillable = [
        'level',
        'code',
        'user_id',
        'msg',
        'entity',
        'entity_id',
        'trace',
        'ip',
    ];

    /**
     * Qué significa cada código.
     *
     * Existe porque el registro se consulta por código y **un número sin descripción no
     * se puede filtrar**: nadie recuerda que el 16012 es el corte de los límites. Estaban
     * repartidos por cuatro documentos distintos de `.tresipunt`, así que aquí queda la
     * lista viva y la documentación pasa a apuntar a ella.
     *
     * Al añadir un `Log::db()` con un código nuevo, se añade aquí. Un código que no esté
     * en esta lista sigue funcionando y se muestra como «Código NNNNN», que es mejor que
     * fallar, pero no se puede buscar por su nombre.
     */
    public const CODIGOS = [
        '15001' => 'Usuario creado',
        '15011' => 'Usuario modificado',
        '16001' => 'Entorno vinculado o desvinculado de una licencia',
        '16002' => 'Borrado de registros del log de la API',
        '16003' => 'Límite de peticiones de la API alcanzado (por IP)',
        '16004' => 'Aviso de límite de la API enviado',
        '16005' => 'Sincronización lanzada desde el Manager',
        '16006' => 'Sincronización desde el Manager fallida',
        '16007' => 'Aviso de bloqueo sin destinatarios configurados',
        '16008' => 'Límite de peticiones de una licencia alcanzado',
        '16009' => 'Resumen diario de bloqueos enviado',
        '16010' => 'Licencia desactivada automáticamente por caducidad',
        '16011' => 'IP bloqueada o desbloqueada a mano',
        '16012' => 'Cambio del modo de los límites de la API (cortar / observar)',
        '16013' => 'Cambio de la retención de los logs de la API',
        '16014' => 'Cambio de los umbrales de los límites de la API',
        '16015' => 'Soporte asignado o modificado en un entorno',
        '16016' => 'Soporte quitado de un entorno',
        '16017' => 'Token de servicios web de un Moodle guardado, reemplazado o borrado',
        '16018' => 'Licencia renovada',
        '16019' => 'Cliente dado de baja o de alta',
        '16020' => 'Dominio de un entorno cambiado',
        '16021' => 'Producto quitado de una licencia',
        '16022' => 'Licencia apagada o vuelta a encender a mano',
        '16023' => 'Usuario del panel borrado',
        '16024' => 'Producto borrado del catálogo',
        // **Estos siete se usaban sin estar aquí**, así que el visor del panel pintaba
        // «Código 16031» en lugar del texto, tanto en la fila como en el desplegable de
        // filtros. El 16027 no lo usa nadie: se salta a propósito para no inventarle un
        // significado.
        '16025' => 'Tipo de entorno borrado del catálogo',
        '16026' => 'Rol borrado',
        '16028' => 'Plugin declarado como propio',
        '16029' => 'Plugin retirado del catálogo de propios',
        '16030' => 'Límite de peticiones de una licencia cambiado',
        '16031' => 'Entorno apagado o vuelto a encender',
        '16032' => 'Datos de uso pedidos desde el Manager',
        '16033' => 'Petición de datos de uso desde el Manager fallida',
        '16034' => 'Versión de contenido borrada',
        '16035' => 'Elemento de una versión de contenido borrado',
        '16036' => 'Tipo de soporte borrado del catálogo',
        // Los tres del catálogo de usos del entorno. **El alta también se registra**, y no
        // solo el borrado como en el resto de catálogos: el `shortname` de un uso es lo que
        // manda cada Moodle en su `sync`, así que un alta con el valor mal escrito se ve
        // como «ningún entorno queda con este uso» y no como un error. El registro es donde
        // se comprueba qué se escribió y quién.
        '16037' => 'Uso de entorno creado en el catálogo',
        '16038' => 'Uso de entorno modificado en el catálogo',
        '16039' => 'Uso de entorno borrado del catálogo',
        // El borrado del registro de cortes. **Con rastro porque no tiene papelera**:
        // `api_blocks` y `api_rate_events` no llevan borrado lógico, así que sin esta línea
        // un borrado en lote de 97 episodios no dejaría constancia de que alguien lo hizo.
        '16040' => 'Episodios de corte borrados del registro',
        // Los dos borrados que se hacen sobre el propio panel. El 16041 es el único
        // apunte que sobrevive a su propio borrado: se escribe después de vaciar.
        '16041' => 'Acciones borradas del registro del panel',
        '16042' => 'Avisos borrados del histórico',
        // Lo único del parque que escribe una persona: la observación de un entorno.
        // Al resolverla se borra de la ficha, así que estos dos apuntes son todo el
        // histórico que queda — por eso llevan el texto dentro y no solo el nombre.
        '16043' => 'Entorno marcado para revisar, o su observación modificada',
        '16044' => 'Observación de un entorno resuelta',
    ];

    /** Niveles, del más grave al más rutinario. */
    public const NIVELES = [
        'danger' => 'Error',
        'warning' => 'Aviso',
        'info' => 'Información',
    ];

    /**
     * Quién lo hizo. Nulo = una tarea programada o la propia API.
     *
     * `nullOnDelete`: si la cuenta se borra la entrada se queda, porque lo que
     * importa es que la acción ocurrió.
     */
    public function user()
    {
        return $this->belongsTo(\App\Models\Auth\User::class, 'user_id');
    }

    /** Texto de quien lo hizo, ya resuelto para pantallas. */
    public function getResponsableAttribute(): string
    {
        if ($this->user) {
            return $this->user->name;
        }

        // No se intenta distinguir el cron de la API: `request()->ip()` devuelve algo
        // en los dos casos —incluso en un comando— así que cualquier heurística ahí
        // miente. Lo que importa es que NO lo hizo una persona.
        return 'Tarea programada o API';
    }

    public function getDescripcionAttribute(): string
    {
        return self::CODIGOS[$this->code] ?? 'Código ' . $this->code;
    }

    /**
     * Enlace a la ficha de la entidad implicada, si se sabe a dónde ir.
     *
     * `entity` y `entity_id` se rellenaban desde el principio y **no se usaban para
     * nada**: el accessor `url` de abajo componía a mano una ruta del tipo
     * `clients/3/edit` que no existe como tal en este proyecto. Aquí se resuelven las
     * entidades que sí tienen pantalla, y para el resto no se ofrece enlace en vez de
     * ofrecer uno roto.
     */
    public function enlace(): ?string
    {
        if (!$this->entity || !$this->entity_id) {
            return null;
        }

        $id = (int) $this->entity_id;

        try {
            return match ($this->entity) {
                'Client' => route('clients.show', $id),
                'Environment' => route('environments.index', ['search' => $id]),
                'LicenseToken' => route('license_tokens.show', $id),
                'EnvironmentSupport' => null,
                default => null,
            };
        } catch (\Throwable) {
            // Una ruta que cambie de nombre no puede tumbar la pantalla del registro.
            return null;
        }
    }

    protected $appends = ['url'];

    public function getUrlAttribute(): string
    {
        if (!$this->entity || !$this->entity_id) {
            return '';
        }

        return strtolower($this->entity) . 's/' . $this->entity_id . '/edit';
    }

    public static function db(
        string $level,
        string $code,
        string $msg,
        ?string $entity = null,
        ?string $id = null,
        ?string $trace = null
    ) {
        self::create([
            'level' => $level,
            'code' => $code,
            // Automático a propósito: si dependiera de que cada sitio lo pase, unos
            // lo pondrían y otros no —que es exactamente lo que pasaba con el nombre
            // dentro del mensaje—. Nulo significa "lo hizo una tarea programada" o la
            // propia API, y eso también es información.
            'user_id' => \Illuminate\Support\Facades\Auth::id(),
            'msg' => $msg,
            'entity' => $entity,
            'entity_id' => $id,
            'trace' => $trace,
            'ip' => request()->ip(),
        ]);
    }
}

