<?php

namespace App\Models\Environments;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;

/**
 * Un uso del entorno: `pro`, `pre`, `develop`, `local`.
 *
 * **No confundir con `Type`.** Son las dos preguntas que se hacen de un sitio y son
 * distintas:
 *
 * - `Type` es **la plataforma**: Moodle LMS, Workplace, WordPress, Goodle. De ella depende
 *   qué productos le son compatibles.
 * - `Env` —esto— es **para qué se usa ese sitio**: producción, su previo, desarrollo, la
 *   máquina de alguien. De ella depende a qué se mira primero cuando algo falla.
 *
 * Era una constante en `Environment` con los cuatro valores escritos a mano; ahora es
 * catálogo. El porqué, en la migración `create_envs_table`.
 *
 * ## El emparejamiento es por `shortname`, no por id
 *
 * `environments.env` guarda la cadena —`"pro"`—, porque **ese valor lo manda cada Moodle**
 * en el payload de la API. Así que aquí no hay clave ajena: hay un catálogo que dice qué
 * cadenas son válidas, cómo se llaman y de qué color se pintan.
 *
 * @property int $id
 * @property string $shortname
 * @property string $name
 * @property string|null $description
 * @property string $color_text
 * @property string $color_bg
 * @property bool $visible_por_defecto
 * @property int $orden
 */
class Env extends Model
{
    use HasFactory, SoftDeletes;

    protected $table = 'envs';

    protected $fillable = [
        'shortname',
        'name',
        'description',
        'color_text',
        'color_bg',
        'visible_por_defecto',
        'orden',
    ];

    protected $casts = [
        'visible_por_defecto' => 'boolean',
        'orden' => 'integer',
    ];

    /** La clave de la caché del catálogo. Una sola, porque el catálogo es uno. */
    private const CLAVE = 'envs:catalogo';

    /**
     * Los colores que se pueden elegir para un uso.
     *
     * **Una paleta cerrada y no un campo de texto libre.** Estas etiquetas se pintan en el
     * listado, en la ficha, en los botones del filtro y en cuatro pantallas más, así que un
     * color escrito a mano —`red`, un hex cualquiera— daría una etiqueta ilegible en el
     * tema oscuro o un contraste que no cumple, y no habría dónde verlo antes de guardar.
     *
     * Son pares texto/fondo del sistema de diseño (`resources/css/design-system`), o sea
     * que **siguen al tema**: si mañana cambia la marca, estas etiquetas cambian con ella.
     * El primero de la lista es el que se ofrece por defecto al crear.
     *
     * @var array<string, array{nombre: string, texto: string, fondo: string}>
     */
    public const PALETA = [
        'verde' => ['nombre' => 'Verde — el de producción', 'texto' => 'var(--teal-700)', 'fondo' => 'var(--teal-100)'],
        'azul' => ['nombre' => 'Azul — el del previo', 'texto' => 'var(--info-500)', 'fondo' => 'var(--info-50)'],
        'gris' => ['nombre' => 'Gris — el de desarrollo', 'texto' => 'var(--neutral-700)', 'fondo' => 'var(--neutral-100)'],
        'gris-suave' => ['nombre' => 'Gris suave — el de local', 'texto' => 'var(--neutral-600)', 'fondo' => 'var(--surface-sunken)'],
        'naranja' => ['nombre' => 'Naranja — el de la marca', 'texto' => 'var(--orange-700)', 'fondo' => 'var(--orange-50)'],
        'ambar' => ['nombre' => 'Ámbar — para avisar', 'texto' => 'var(--warning-500)', 'fondo' => 'var(--warning-50)'],
        'rojo' => ['nombre' => 'Rojo — para lo delicado', 'texto' => 'var(--danger-500)', 'fondo' => 'var(--danger-50)'],
        'verde-claro' => ['nombre' => 'Verde claro', 'texto' => 'var(--success-500)', 'fondo' => 'var(--success-50)'],
    ];

    /**
     * La clave de la paleta que corresponde a los colores guardados.
     *
     * Para que el formulario de edición llegue con el color puesto. Si la fila lleva un par
     * que no está en la paleta —lo pudo poner una migración anterior—, devuelve `null` y el
     * formulario lo dice en lugar de cambiárselo por sorpresa.
     */
    public function claveDePaleta(): ?string
    {
        foreach (self::PALETA as $clave => $color) {
            if ($color['texto'] === $this->color_text && $color['fondo'] === $this->color_bg) {
                return $clave;
            }
        }

        return null;
    }

    /**
     * El color con el que se pinta este uso: `[texto, fondo]`.
     *
     * @return array{string, string}
     */
    public function color(): array
    {
        return [$this->color_text, $this->color_bg];
    }

    /** Los entornos que declaran este uso. Es por `shortname`, no por clave ajena. */
    public function environments()
    {
        return Environment::where('env', $this->shortname);
    }

    /**
     * El catálogo entero, ordenado, **cacheado**.
     *
     * Se lee en cada `render()` de media docena de pantallas —el listado de entornos lo
     * consulta para pintar cuatro botones y cada fila para su etiqueta—, y cambia tres
     * veces al año. Era una constante de PHP: convertirla en una consulta por fila sería
     * cambiar un problema por otro.
     *
     * La caché se tira sola al guardar o borrar cualquier fila, en `booted()`.
     *
     * @return Collection<int, self>
     */
    public static function catalogo(): Collection
    {
        return Cache::rememberForever(
            self::CLAVE,
            fn () => self::query()->orderBy('orden')->orderBy('name')->get()
        );
    }

    /**
     * `[shortname => nombre]`, que es la forma que consumían las vistas y los `<select>`.
     *
     * Es la sustituta exacta de la vieja constante `Environment::ENVIRONMENTS`, y por eso
     * mantiene su forma: los formularios, el asistente y el selector de licencias la
     * recorren con `$key => $label`.
     *
     * @return array<string, string>
     */
    public static function etiquetas(): array
    {
        return self::catalogo()->pluck('name', 'shortname')->all();
    }

    /**
     * `[shortname => [texto, fondo]]`.
     *
     * @return array<string, array{string, string}>
     */
    public static function colores(): array
    {
        return self::catalogo()
            ->mapWithKeys(fn (self $env) => [$env->shortname => $env->color()])
            ->all();
    }

    /**
     * El color de un uso por su `shortname`, con una red de seguridad neutra.
     *
     * Se llama `colorDe` y no `color` porque `color()` es el de **esta** fila: dos
     * métodos con el mismo nombre, uno estático y otro de instancia, no se pueden tener.
     *
     * **La red importa**: `environments.env` es una cadena libre a efectos de esta tabla,
     * así que puede haber un valor guardado cuyo uso se haya borrado del catálogo. Pintarlo
     * en gris es correcto; reventar la ficha del entorno, no.
     *
     * @return array{string, string}
     */
    public static function colorDe(?string $shortname): array
    {
        return self::colores()[$shortname] ?? ['var(--neutral-600)', 'var(--surface-sunken)'];
    }

    /**
     * Los usos que salen marcados de entrada en el filtro del listado de entornos.
     *
     * **Esto es lo que antes era la constante `ENVS_POR_DEFECTO`**, y por qué está aquí:
     * el listado esconde los `local` porque en el parque real son ruido de desarrollo, y
     * eso es una decisión de quien gestiona el parque, no del código.
     *
     * **Nunca devuelve una lista vacía.** Si alguien desmarca los cuatro en el catálogo, el
     * filtro se quedaría sin ningún uso puesto y el listado no podría enseñar nada: la
     * pantalla no tendría ninguna salida que no fuera adivinar la URL. En ese caso valen
     * todos, que es la interpretación inofensiva de «no se ha decidido nada».
     *
     * @return list<string>
     */
    public static function visiblesPorDefecto(): array
    {
        $visibles = self::catalogo()
            ->where('visible_por_defecto', true)
            ->pluck('shortname')
            ->all();

        return $visibles !== [] ? array_values($visibles) : self::todos();
    }

    /**
     * Todos los `shortname` del catálogo, en el orden del catálogo.
     *
     * El orden **es el contrato**: es el de los botones del filtro y el orden canónico con
     * el que viaja `env` en la URL.
     *
     * @return list<string>
     */
    public static function todos(): array
    {
        return self::catalogo()->pluck('shortname')->values()->all();
    }

    /** ¿Es un `env` que el catálogo reconoce? */
    public static function existe(string $shortname): bool
    {
        return in_array($shortname, self::todos(), true);
    }

    /**
     * ¿Es la fila de la que depende un índice de la base?
     *
     * **`local` no es un uso cualquiera.** `environments.domain_unique` es una columna
     * generada definida como `CASE WHEN env = 'local' THEN NULL ELSE domain END`, y el
     * índice único del dominio cuelga de ella: los sitios locales están exentos porque
     * varios desarrolladores comparten `moodle.test`.
     *
     * La comparación es **contra la cadena literal**, así que renombrar este `shortname`
     * desde el catálogo dejaría a los locales nuevos sin la exención —y el error sería una
     * clave duplicada de dominio que no menciona `env` en ninguna parte—, y borrar la fila
     * dejaría el panel sin ninguna forma de dar de alta un sitio exento.
     *
     * Por eso su `shortname` es fijo y su fila no se borra. Todo lo demás —nombre, colores,
     * descripción, si sale marcado en el filtro— sí se puede cambiar: nada de eso viaja a
     * la base.
     */
    public function esExentoDelIndice(): bool
    {
        return $this->shortname === Environment::USO_EXENTO_DEL_INDICE;
    }

    /** Cuántos entornos declaran este uso. Se consulta en vivo: ver `BorrarUsoModal`. */
    public function cuantosEntornos(): int
    {
        return $this->environments()->count();
    }

    public static function olvidarCatalogo(): void
    {
        Cache::forget(self::CLAVE);
    }

    protected static function booted(): void
    {
        // **La caché se tira desde el modelo y no desde las pantallas.** Si dependiera de
        // que cada formulario se acuerde, la primera pantalla que no lo hiciera dejaría el
        // panel entero enseñando un catálogo viejo sin ninguna pista del motivo.
        foreach (['saved', 'deleted', 'restored'] as $evento) {
            static::$evento(fn () => self::olvidarCatalogo());
        }
    }
}
