<?php

namespace App\Support;

/**
 * Un color estable para cada acción de la API.
 *
 * **Por qué.** En el visor todas las acciones se pintaban del mismo gris, así que una
 * tabla de treinta filas de `licence`, `sync`, `features` y `products` no se podía
 * recorrer de un vistazo: había que leer cada nombre. Con un color por acción, el ojo
 * agrupa antes de leer.
 *
 * **Por qué no basta con hashear el nombre.** Era la primera versión: `crc32` del texto
 * repartido sobre una paleta. Con 13 acciones y 20 colores, la probabilidad de que dos
 * caigan en el mismo es del **98 %** —la paradoja del cumpleaños—, y se notó enseguida:
 * `licence` y `products` salían iguales. Medido con cinco funciones distintas, la mejor
 * dejaba 11 de 13 separadas:
 *
 * | Función | Distintas | Choques |
 * |---|---|---|
 * | Suma de caracteres | 8/13 | `licence`=`features`, `scss`=`products`… |
 * | Suma ponderada | 10/13 | `licence`=`data`=`resources` |
 * | `crc32` | 9/13 | `resources`=`products`, `js`=`tutorials` |
 * | `crc32` + longitud | 11/13 | `sync`=`js`, `data`=`tutorials` |
 * | djb2 | 11/13 | `licence`=`resources`, `sync`=`setup` |
 *
 * Ninguna sirve, y no es cuestión de encontrar una mejor: **el problema es el reparto, no
 * la función**.
 *
 * **Así que las conocidas van fijadas.** Las acciones de la API son una lista cerrada y
 * documentada —el contrato con el plugin—, así que cada una tiene su color asignado y no
 * puede chocar con otra. Lo que se hashea es solo lo que no conocemos, y **sobre los
 * colores que sobran**, para que una acción nueva no pueda robarle el color a `licence`.
 *
 * **El color no es un hexadecimal al azar.** De cada entrada de la paleta se sortea solo
 * el matiz; la saturación y la luminosidad son fijas, así que todos pesan lo mismo y el
 * texto siempre contrasta con su fondo. Y se saltan tres zonas del círculo: el rojo es
 * «error», el verde «correcta» y el naranja es el color de la marca. Una acción en rojo se
 * lee como una acción que ha fallado, que es justo el malentendido que este color venía a
 * evitar.
 */
class ColorDeAccion
{
    /**
     * Veinte matices, repartidos por el círculo **saltando** el rojo (0–45), el naranja
     * (hasta 45) y el verde (95–155).
     *
     * Los cuatro primeros son la zona oliva/amarilla y el resto va del turquesa al
     * magenta. Ninguno llega al 350: a partir de ahí ya se lee como rojo.
     *
     * @var list<int>
     */
    private const MATICES = [
        50, 62, 74, 86,
        160, 172, 184, 196, 208, 220, 232, 244, 256, 268, 280, 292, 304, 316, 328, 340,
    ];

    /**
     * Las acciones del contrato, con su color fijado.
     *
     * **El orden es lo que garantiza que no choquen**: cada una tiene su índice en la
     * paleta. Están las 13 de la API más `blocks`, y quedan seis colores libres para las
     * que lleguen.
     *
     * Si se añade una acción al contrato, se añade aquí con el siguiente índice libre.
     *
     * @var array<string,int>
     */
    private const CONOCIDAS = [
        'licence' => 4,     // turquesa: es la más frecuente, el 70 % de las peticiones
        'sync' => 5,
        'data' => 6,
        'setup' => 7,
        'scss' => 8,
        'scss-cdn' => 9,
        'js' => 10,
        'features' => 11,
        'tutorials' => 12,
        'resources' => 13,
        'products' => 14,
        'stats' => 15,
        'blocks' => 16,
        // 0–3 (la zona oliva) se reservan para las acciones que no conocemos, junto con
        // 17, 18 y 19: así una acción nueva se distingue **también** por estar en una
        // familia de color distinta de las del contrato.
    ];

    /** Los huecos que quedan para lo que no está en el contrato. */
    private const LIBRES = [0, 1, 2, 3, 17, 18, 19];

    /** Fijas para que todos los colores pesen igual y el texto siempre contraste. */
    private const SATURACION_TEXTO = 55;
    private const LUZ_TEXTO = 34;
    private const SATURACION_FONDO = 70;
    private const LUZ_FONDO = 94;

    /**
     * El par de colores de una acción: texto y fondo del mismo matiz.
     *
     * @return array{texto: string, fondo: string}
     */
    public static function de(?string $accion): array
    {
        if ($accion === null || trim($accion) === '') {
            // Sin acción no hay nada que agrupar: gris del sistema, no un color inventado.
            return ['texto' => 'var(--text-muted)', 'fondo' => 'var(--surface-sunken)'];
        }

        $matiz = self::matiz($accion);

        return [
            'texto' => 'hsl(' . $matiz . ', ' . self::SATURACION_TEXTO . '%, ' . self::LUZ_TEXTO . '%)',
            'fondo' => 'hsl(' . $matiz . ', ' . self::SATURACION_FONDO . '%, ' . self::LUZ_FONDO . '%)',
        ];
    }

    /**
     * El matiz de una acción: fijado si está en el contrato, hasheado si no.
     *
     * `crc32` y no `rand`: la misma cadena da siempre el mismo número, que es todo el
     * punto —el color de una acción tiene que ser el mismo hoy, mañana y en la pantalla de
     * al lado—. No hace falta que sea criptográfico: solo tiene que repartir.
     */
    public static function matiz(string $accion): int
    {
        $clave = trim(mb_strtolower($accion));

        if (isset(self::CONOCIDAS[$clave])) {
            return self::MATICES[self::CONOCIDAS[$clave]];
        }

        $hueco = self::LIBRES[crc32($clave) % count(self::LIBRES)];

        return self::MATICES[$hueco];
    }

    /** Las acciones con color fijo, para los tests y para documentarlo. */
    public static function conocidas(): array
    {
        return array_keys(self::CONOCIDAS);
    }
}
