<?php

namespace App\Helpers;

use App\Models\Mobiangra\MobiangraLinha;
use Illuminate\Support\Collection;

/**
 * Busca tolerante de linhas MobiAngra (acentos e pequenos erros de digitação).
 */
class MobiangraBuscaLinhasHelper
{
    public static function normalizar(string $texto): string
    {
        $texto = mb_strtolower(trim($texto), 'UTF-8');

        if ($texto === '') {
            return '';
        }

        $ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $texto);

        if ($ascii !== false) {
            $texto = $ascii;
        }

        $texto = preg_replace('/[^a-z0-9\s\-\/]/', ' ', $texto) ?? $texto;

        return trim(preg_replace('/\s+/', ' ', $texto) ?? $texto);
    }

    /**
     * @param  array<int, string|null>  $campos
     */
    public static function corresponde(?string $termo, array $campos): bool
    {
        $termo = trim((string) $termo);

        if ($termo === '') {
            return true;
        }

        $tokens = array_filter(explode(' ', self::normalizar($termo)));

        if ($tokens === []) {
            return true;
        }

        $haystack = self::normalizar(implode(' ', array_filter(array_map(
            static fn ($c) => $c !== null ? (string) $c : '',
            $campos
        ))));

        if ($haystack === '') {
            return false;
        }

        foreach ($tokens as $token) {
            if (! self::tokenCorresponde($haystack, $token)) {
                return false;
            }
        }

        return true;
    }

    private static function tokenCorresponde(string $haystack, string $token): bool
    {
        if ($token === '') {
            return true;
        }

        if (str_contains($haystack, $token)) {
            return true;
        }

        $maxDist = self::maxEditDistance(mb_strlen($token));

        if ($maxDist === 0) {
            return false;
        }

        foreach (explode(' ', $haystack) as $parte) {
            if ($parte === '' || mb_strlen($parte) < 2) {
                continue;
            }

            if (str_contains($parte, $token) || str_contains($token, $parte)) {
                return true;
            }

            if (abs(mb_strlen($parte) - mb_strlen($token)) <= $maxDist
                && self::levenshtein($parte, $token) <= $maxDist) {
                return true;
            }
        }

        if (mb_strlen($token) >= 6 && abs(mb_strlen($haystack) - mb_strlen($token)) <= $maxDist + 2) {
            return self::levenshtein($haystack, $token) <= $maxDist;
        }

        return false;
    }

    private static function maxEditDistance(int $len): int
    {
        return match (true) {
            $len <= 3 => 0,
            $len <= 5 => 1,
            $len <= 8 => 2,
            default => 3,
        };
    }

    private static function levenshtein(string $a, string $b): int
    {
        $m = mb_strlen($a);
        $n = mb_strlen($b);

        if ($m === 0) {
            return $n;
        }

        if ($n === 0) {
            return $m;
        }

        $dp = range(0, $n);

        for ($i = 1; $i <= $m; $i++) {
            $prev = $dp[0];
            $dp[0] = $i;
            $charA = mb_substr($a, $i - 1, 1);

            for ($j = 1; $j <= $n; $j++) {
                $tmp = $dp[$j];
                $cost = $charA === mb_substr($b, $j - 1, 1) ? 0 : 1;
                $dp[$j] = min($dp[$j] + 1, $dp[$j - 1] + 1, $prev + $cost);
                $prev = $tmp;
            }
        }

        return $dp[$n];
    }

    /**
     * Resolve índice da linha em uma coleção pelo termo (?linha= na URL).
     *
     * @param  Collection<int, MobiangraLinha>|array<int, MobiangraLinha>  $linhas
     */
    public static function indicePorTermo(Collection|array $linhas, ?string $termo): ?int
    {
        $termo = trim((string) $termo);

        if ($termo === '') {
            return null;
        }

        $lista = $linhas instanceof Collection ? $linhas->values() : array_values($linhas);

        foreach ($lista as $idx => $linha) {
            if (strcasecmp($linha->codigo, $termo) === 0) {
                return (int) $idx;
            }
        }

        foreach ($lista as $idx => $linha) {
            if (self::corresponde($termo, [
                $linha->codigo,
                $linha->nome,
                $linha->nome_completo,
            ])) {
                return (int) $idx;
            }
        }

        return null;
    }
}
