<?php
declare(strict_types=1);

/** Funções de texto: nomes de coluna, números, meses e nomes de arquivo. */
final class Texto
{
    private const ACENTOS = [
        'á' => 'a', 'à' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a',
        'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e',
        'í' => 'i', 'ì' => 'i', 'î' => 'i', 'ï' => 'i',
        'ó' => 'o', 'ò' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o',
        'ú' => 'u', 'ù' => 'u', 'û' => 'u', 'ü' => 'u',
        'ç' => 'c', 'ñ' => 'n', 'ý' => 'y',
        'Á' => 'A', 'À' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A',
        'É' => 'E', 'È' => 'E', 'Ê' => 'E', 'Ë' => 'E',
        'Í' => 'I', 'Ì' => 'I', 'Î' => 'I', 'Ï' => 'I',
        'Ó' => 'O', 'Ò' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O',
        'Ú' => 'U', 'Ù' => 'U', 'Û' => 'U', 'Ü' => 'U',
        'Ç' => 'C', 'Ñ' => 'N', 'Ý' => 'Y',
    ];

    /** Erros do Excel que contam como campo vazio (em português, espanhol e inglês). */
    private const ERROS_EXCEL = [
        '#N/A', '#N/D', '#REF!', '#VALUE!', '#VALOR!', '#DIV/0!', '#NAME?', '#NOME?', '#NOMBRE?',
        '#NULL!', '#NULO!', '#NUM!', '#NÚM!', '#SPILL!', '#CALC!', '#GETTING_DATA',
    ];

    /** Nome de coluna comparável: sem acentos, maiúsculas, qualquer separador vira um espaço. */
    public static function normalizaCabecalho(string $s): string
    {
        $s = self::semAcento($s);
        $s = strtoupper($s);
        $s = preg_replace('/[\s\x{00A0}_.\-\/]+/u', ' ', $s) ?? $s;
        return trim($s);
    }

    /** Texto limpo para comparar valores (sem acentos, maiúsculas, espaços simples). */
    public static function chave(string $s): string
    {
        $s = self::semAcento($s);
        $s = strtoupper($s);
        $s = preg_replace('/[\s\x{00A0}]+/u', ' ', $s) ?? $s;
        return trim($s);
    }

    /** Tira acentos, inclusive os "soltos" (letra + acento separado, como o macOS grava nomes de arquivo). */
    public static function semAcento(string $s): string
    {
        $s = strtr($s, self::ACENTOS);
        return preg_replace('/\p{Mn}+/u', '', $s) ?? $s;
    }

    /** Código de aliado digitado ou lido: sem espaços, maiúsculo e, se for só número, sem zeros à esquerda. */
    public static function codigoAliado(mixed $v): string
    {
        $c = strtoupper(self::codigo($v));
        $c = preg_replace('/[\s\x{00A0}]+/u', '', $c) ?? $c;
        if (preg_match('/^\d+$/', $c)) {
            $c = ltrim($c, '0');
            return $c === '' ? '0' : $c;
        }
        return $c;
    }

    /** Tira espaços nas pontas, inclusive o espaço invisível (NBSP) que o Excel às vezes deixa. */
    public static function apara(string $s): string
    {
        return preg_replace('/^[\s\x{00A0}\x{200B}\x{FEFF}]+|[\s\x{00A0}\x{200B}\x{FEFF}]+$/u', '', $s) ?? trim($s);
    }

    /** true se a célula conta como vazia: nula, só espaços ou erro do Excel. */
    public static function vazio(mixed $v): bool
    {
        if ($v === null) {
            return true;
        }
        if (is_string($v)) {
            $t = self::apara($v);
            if ($t === '') {
                return true;
            }
            return in_array(strtoupper($t), self::ERROS_EXCEL, true);
        }
        return false;
    }

    /**
     * Converte para número. Aceita célula numérica e texto como "$1.234,56", "1,234.56",
     * "18,50", "-2.774,55", "US$ 10". Devolve null se não for número.
     */
    public static function numero(mixed $v): ?float
    {
        if (is_int($v) || is_float($v)) {
            return is_finite((float) $v) ? (float) $v : null;
        }
        if (!is_string($v)) {
            return null;
        }
        $s = self::apara($v);
        if ($s === '') {
            return null;
        }
        $negativo = false;
        if (preg_match('/^\((.*)\)$/', $s, $m)) { // (123,45) = negativo
            $negativo = true;
            $s = $m[1];
        }
        $s = preg_replace('/(US\$|U\$S|USD|\$|\s|\x{00A0})/iu', '', $s) ?? $s;
        if (str_starts_with($s, '-')) {
            $negativo = !$negativo;
            $s = substr($s, 1);
        }
        if ($s === '' || !preg_match('/^[\d.,]+$/', $s)) {
            return null;
        }
        $ultPonto = strrpos($s, '.');
        $ultVirgula = strrpos($s, ',');
        if ($ultPonto !== false && $ultVirgula !== false) {
            // o último separador é o decimal
            if ($ultVirgula > $ultPonto) {
                $s = str_replace('.', '', $s);
                $s = str_replace(',', '.', $s);
            } else {
                $s = str_replace(',', '', $s);
            }
        } elseif ($ultVirgula !== false) {
            // só vírgula: decimal (18,50), a não ser que seja milhar com grupos de 3 (1,234,567)
            if (substr_count($s, ',') > 1) {
                $s = str_replace(',', '', $s);
            } else {
                $s = str_replace(',', '.', $s);
            }
        } elseif ($ultPonto !== false && substr_count($s, '.') > 1) {
            $s = str_replace('.', '', $s); // 1.234.567
        }
        if (!is_numeric($s)) {
            return null;
        }
        $n = (float) $s;
        return $negativo ? -$n : $n;
    }

    /** Número do mês pelo nome (espanhol, português ou inglês, completo ou abreviado), ou null. */
    public static function mesPeloNome(string $s): ?int
    {
        static $mapa = null;
        if ($mapa === null) {
            $nomes = [
                1 => ['ENERO', 'JANEIRO', 'JANUARY', 'ENE', 'JAN'],
                2 => ['FEBRERO', 'FEVEREIRO', 'FEBRUARY', 'FEB', 'FEV'],
                3 => ['MARZO', 'MARCO', 'MARCH', 'MAR'],
                4 => ['ABRIL', 'APRIL', 'ABR', 'APR'],
                5 => ['MAYO', 'MAIO', 'MAY', 'MAI'],
                6 => ['JUNIO', 'JUNHO', 'JUNE', 'JUN'],
                7 => ['JULIO', 'JULHO', 'JULY', 'JUL'],
                8 => ['AGOSTO', 'AUGUST', 'AGO', 'AUG'],
                9 => ['SEPTIEMBRE', 'SETIEMBRE', 'SETEMBRO', 'SEPTEMBER', 'SEP', 'SET', 'SEPT'],
                10 => ['OCTUBRE', 'OUTUBRO', 'OCTOBER', 'OCT', 'OUT'],
                11 => ['NOVIEMBRE', 'NOVEMBRO', 'NOVEMBER', 'NOV'],
                12 => ['DICIEMBRE', 'DEZEMBRO', 'DECEMBER', 'DIC', 'DEZ', 'DEC'],
            ];
            $mapa = [];
            foreach ($nomes as $n => $lista) {
                foreach ($lista as $nome) {
                    $mapa[$nome] = $n;
                }
            }
        }
        $k = self::chave($s);
        $k = rtrim($k, '.');
        return $mapa[$k] ?? null;
    }

    /**
     * Mês (1 a 12) do que veio na coluna TX_MES: nome ou abreviatura em espanhol, português ou inglês
     * ("OCTUBRE", "Outubro", "oct", "Octubre 2026", "OCT-26"), número ("10"), "10/2026", "2026-10",
     * data ("01/10/2026") ou data do Excel. Devolve null se não der para saber o mês.
     */
    public static function mesDoTexto(mixed $v): ?int
    {
        $doNumero = static function (float $n): ?int {
            if ($n == floor($n) && $n >= 1 && $n <= 12) {
                return (int) $n;
            }
            if ($n > 30000 && $n < 80000) { // data guardada pelo Excel como número de série
                return (int) gmdate('n', (int) round(($n - 25569) * 86400));
            }
            return null;
        };
        if (is_int($v) || is_float($v)) {
            return is_finite((float) $v) ? $doNumero((float) $v) : null;
        }
        if (!is_string($v)) {
            return null;
        }
        $s = self::chave($v);
        if ($s === '') {
            return null;
        }
        $nome = self::mesPeloNome($s);
        if ($nome !== null) {
            return $nome;
        }
        if (preg_match('/^-?\d+(?:[.,]0+)?$/', $s)) {
            return $doNumero((float) str_replace(',', '.', $s));
        }
        $ok = static fn (int $m): ?int => $m >= 1 && $m <= 12 ? $m : null;
        if (preg_match('/^(\d{4})[-\/.](\d{1,2})(?:[-\/.]\d{1,2})?$/', $s, $m)) {
            return $ok((int) $m[2]);
        }
        if (preg_match('/^(\d{1,2})[-\/.](\d{4})$/', $s, $m)) {
            return $ok((int) $m[1]);
        }
        if (preg_match('/^(\d{1,2})[-\/.](\d{1,2})[-\/.](\d{2,4})$/', $s, $m)) {
            return $ok((int) $m[2]); // dia/mês/ano
        }
        $achados = [];
        foreach (preg_split('/[\s\/\-.,_]+/', $s) ?: [] as $parte) {
            $m = $parte !== '' ? self::mesPeloNome($parte) : null;
            if ($m !== null) {
                $achados[$m] = true;
            }
        }
        return count($achados) === 1 ? (int) array_key_first($achados) : null;
    }

    /** Meses por extenso em português, sem acento (como ficam no arquivo pronto). */
    public const MESES_PT = [1 => 'JANEIRO', 2 => 'FEVEREIRO', 3 => 'MARCO', 4 => 'ABRIL', 5 => 'MAIO', 6 => 'JUNHO',
        7 => 'JULHO', 8 => 'AGOSTO', 9 => 'SETEMBRO', 10 => 'OUTUBRO', 11 => 'NOVEMBRO', 12 => 'DEZEMBRO'];

    /** Países com grafia diferente em português (depois de tirar acentos e passar para maiúsculas). */
    private const PAISES_PT = ['URUGUAY' => 'URUGUAI', 'PARAGUAY' => 'PARAGUAI', 'ECUADOR' => 'EQUADOR'];

    /** Maiúsculas e sem acento: "Câmera Bullet" -> "CAMERA BULLET"; "D´Santiago" -> "D'SANTIAGO". */
    public static function maiusculaSemAcento(string $s): string
    {
        $s = strtr(self::semAcento(self::apara($s)), ['´' => "'", '¨' => '']);
        // o símbolo de mícron (50µ) continua µ: em maiúscula ele viraria a letra grega Μ
        return implode('µ', array_map(static fn (string $p): string => mb_strtoupper($p, 'UTF-8'), explode('µ', $s)));
    }

    /** Texto como fica no arquivo pronto: maiúsculas, sem acento e com o nome do país em português. */
    public static function nomePadrao(string $s): string
    {
        $t = self::maiusculaSemAcento($s);
        return self::PAISES_PT[$t] ?? $t;
    }

    /** Texto seguro para nome de arquivo: ASCII, sem espaços nem símbolos. */
    public static function nomeDeArquivo(string $s, int $max = 40): string
    {
        $s = strtr($s, self::ACENTOS);
        $s = preg_replace('/[^A-Za-z0-9]+/', '_', $s) ?? '';
        $s = trim($s, '_');
        return substr($s === '' ? 'arquivo' : $s, 0, $max);
    }

    /** Código de item ou de aliado como texto: 4541073.0 -> "4541073", sem espaços invisíveis. */
    public static function codigo(mixed $v): string
    {
        if (is_int($v)) {
            return (string) $v;
        }
        if (is_float($v)) {
            return floor($v) === $v && abs($v) < 1e15 ? (string) (int) $v : (string) $v;
        }
        if (is_bool($v) || $v === null) {
            return '';
        }
        $s = self::apara((string) $v);
        if (preg_match('/^(\d+)\.0+$/', $s, $m)) {
            return $m[1];
        }
        return $s;
    }
}
