<?php

namespace App\Support;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

/**
 * Búsqueda aproximada de entornos por nombre y dominio, para la API de administración
 * (PRODSECU-215).
 *
 * Quien busca un sitio lo escribe como lo tiene en la mano: con o sin `https://`, con o
 * sin `www.`, con la barra del final, o solo con un trozo del nombre. Un `LIKE` sobre el
 * dominio tal cual no encuentra `campus.uejemplo.es` si se busca `https://campus.uejemplo.es/`.
 *
 * Así que se compara contra el **host** —el dominio sin protocolo, sin `www.` ni barra— y
 * contra el nombre, y se ordena por cercanía. Si nada coincide, se sugieren los más
 * parecidos aunque haya erratas: es una búsqueda para encontrar, no para comprobar.
 *
 * **Se puntúa en PHP y no en SQL** porque la similitud no existe igual en MySQL y en sqlite
 * —donde corren los tests—, y los entornos son unos cientos: se leen solo `id`, `name` y
 * `domain` de los que pasan el resto de filtros.
 */
final class BusquedaDeEntornos
{
    /** Por debajo de este parecido (0–100) no se sugiere: sería ruido. */
    private const PARECIDO_MINIMO = 60;

    /** Lo más largo que se compara por parecido. */
    private const LARGO_MAXIMO = 100;

    /**
     * Los ids de los entornos que encajan con `$termino`, del más cercano al más lejano.
     *
     * @param  Builder  $consulta  Con el resto de filtros ya puestos.
     * @return list<int>
     */
    public static function ids(Builder $consulta, string $termino): array
    {
        $buscado = self::host($termino);

        if ($buscado === '') {
            return [];
        }

        $palabras = array_values(array_filter(preg_split('/[\s.\-_\/]+/', $buscado) ?: []));

        /** @var Collection<int, array{id: int, puntos: float, name: string}> $puntuados */
        $puntuados = (clone $consulta)
            ->get(['id', 'name', 'domain'])
            ->map(fn ($entorno) => [
                'id' => (int) $entorno->id,
                'name' => (string) $entorno->name,
                'puntos' => self::puntos($buscado, $palabras, self::host((string) $entorno->domain), mb_strtolower((string) $entorno->name)),
            ]);

        $encontrados = $puntuados->filter(fn ($fila) => $fila['puntos'] > 50);

        // Nada coincide: se sugieren los parecidos.
        if ($encontrados->isEmpty()) {
            $encontrados = $puntuados->filter(fn ($fila) => $fila['puntos'] > 0);
        }

        return $encontrados
            ->sortBy([['puntos', 'desc'], ['name', 'asc']])
            ->pluck('id')
            ->values()
            ->all();
    }

    /**
     * El dominio reducido a lo que se compara: minúsculas, sin protocolo, sin `www.` y sin
     * barra final. Un texto que no es dominio pasa igual, recortado.
     */
    public static function host(string $texto): string
    {
        $texto = mb_strtolower(trim($texto));
        $texto = preg_replace('#^[a-z][a-z0-9+.\-]*://#', '', $texto) ?? $texto;
        $texto = preg_replace('#^www\.#', '', $texto) ?? $texto;

        return rtrim($texto, '/');
    }

    /**
     * Cuánto se parece un entorno a lo buscado.
     *
     * 100 el mismo host · 90 el host empieza así · 80 lo contiene · 70 el nombre lo
     * contiene · 60 todas las palabras están en el nombre o el host · por debajo de 50, el
     * parecido de texto (erratas), solo si llega al mínimo.
     *
     * @param  list<string>  $palabras
     */
    private static function puntos(string $buscado, array $palabras, string $host, string $nombre): float
    {
        if ($host === $buscado) {
            return 100;
        }

        if ($host !== '' && str_starts_with($host, $buscado)) {
            return 90;
        }

        if ($host !== '' && str_contains($host, $buscado)) {
            return 80;
        }

        if (str_contains($nombre, $buscado)) {
            return 70;
        }

        if ($palabras !== [] && collect($palabras)->every(fn ($palabra) => str_contains($nombre . ' ' . $host, $palabra))) {
            return 60;
        }

        $parecido = max(self::parecido($buscado, $host), self::parecido($buscado, $nombre));

        return $parecido >= self::PARECIDO_MINIMO ? $parecido / 2 : 0;
    }

    private static function parecido(string $a, string $b): float
    {
        // `similar_text` es cuadrático: con textos largos cada llamada cuesta milisegundos,
        // y se hace dos veces por entorno. Por encima de 100 caracteres no se sugiere por
        // parecido (auditoría PRODSECU-215).
        if ($a === '' || $b === '' || strlen($a) > self::LARGO_MAXIMO || strlen($b) > self::LARGO_MAXIMO) {
            return 0;
        }

        similar_text($a, $b, $porcentaje);

        return $porcentaje;
    }
}
