<?php

namespace App\Support;

/**
 * Coincidencia tolerante a errores tipográficos para comandos de texto en WhatsApp (Levenshtein).
 * Normaliza a ASCII para comparar «menú»/«menu» y teclados con fallos.
 */
final class WhatsAppKeywordFuzzy
{
    /** Máxima distancia por longitud del candidato (bytes ASCII tras fold) */
    public static function maxDistanceForLength(int $len): int
    {
        if ($len <= 3) {
            return 1;
        }
        if ($len <= 8) {
            return 2;
        }

        return 3;
    }

    /**
     * Minúsculas + transliteración a ASCII para Levenshtein (evita problemas con UTF-8 en bytes).
     */
    public static function asciiFold(string $s): string
    {
        $s = mb_strtolower(trim($s), 'UTF-8');
        if ($s === '') {
            return '';
        }
        $a = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
        $a = is_string($a) ? $a : $s;
        $a = strtolower($a);

        return preg_replace('/[^a-z0-9\s:]/', '', $a) ?? $a;
    }

    /**
     * @param  array<int, string>  $candidates  Frases o palabras aceptadas (se comparan ya folded)
     * @return string|null  El primer candidato que gana el emparejamiento (el string original pasado)
     */
    public static function bestMatch(string $input, array $candidates): ?string
    {
        $norm = self::asciiFold($input);
        if ($norm === '') {
            return null;
        }

        $best = null;
        $bestDist = PHP_INT_MAX;

        foreach ($candidates as $cand) {
            $c = self::asciiFold($cand);
            if ($c === '') {
                continue;
            }
            if ($norm === $c) {
                return $cand;
            }
            $len = max(strlen($norm), strlen($c));
            $max = self::maxDistanceForLength($len);
            if (strlen($norm) > 255 || strlen($c) > 255) {
                continue;
            }
            $d = levenshtein($norm, $c);
            if ($d === -1) {
                continue;
            }
            if ($d <= $max && $d < $bestDist) {
                $bestDist = $d;
                $best = $cand;
            }
        }

        return $best;
    }

    /**
     * Elige la clave canónica con **menor** distancia mínima a cualquier alias (incluida la clave).
     *
     * @param  array<string, list<string>>  $map  canonical_key => aliases adicionales
     * @param  list<string>  $priority  Si hay empate en la distancia, gana quien aparezca primero aquí
     */
    public static function matchCanonical(string $input, array $map, array $priority = []): ?string
    {
        $norm = self::asciiFold($input);
        if ($norm === '') {
            return null;
        }

        /** @var array<string, int> $minDistPorClave distancia mínima válida por clave canónica */
        $minDistPorClave = [];

        foreach ($map as $canonical => $aliases) {
            $best = PHP_INT_MAX;
            foreach (array_merge([$canonical], $aliases) as $phrase) {
                $c = self::asciiFold($phrase);
                if ($c === '') {
                    continue;
                }
                if (strlen($norm) > 255 || strlen($c) > 255) {
                    continue;
                }
                $d = levenshtein($norm, $c);
                if ($d === -1) {
                    continue;
                }
                $len = max(strlen($norm), strlen($c));
                $max = self::maxDistanceForLength($len);
                if ($d <= $max) {
                    $best = min($best, $d);
                }
            }
            if ($best !== PHP_INT_MAX) {
                $minDistPorClave[$canonical] = $best;
            }
        }

        if ($minDistPorClave === []) {
            return null;
        }

        $globalMin = min($minDistPorClave);
        $candidatos = array_keys(array_filter($minDistPorClave, fn (int $d): bool => $d === $globalMin));
        if (count($candidatos) === 1) {
            return $candidatos[0];
        }

        $order = $priority !== [] ? $priority : array_keys($map);
        $rank = array_flip($order);
        usort($candidatos, function (string $a, string $b) use ($rank): int {
            return ($rank[$a] ?? 99) <=> ($rank[$b] ?? 99);
        });

        return $candidatos[0] ?? null;
    }

    /**
     * Igual que bestMatch pero devuelve bool.
     *
     * @param  array<int, string>  $candidates
     */
    public static function matches(string $input, array $candidates): bool
    {
        return self::bestMatch($input, $candidates) !== null;
    }
}
