"""valores.py: reads numbers and money said in Portuguese (PT-BR speech and on-screen text).

Library used by motion_sugerir.py. Normalizes accents and case (`norm`) and parses numbers written or spoken
("1.200", "vinte e cinco") and money ("R$ 5 mil", "10k", "cinco mil reais", "meio milhão", "dois mil e
quinhentos", the slang "milão", "seis dígitos"). Counts are not money: "10 mil seguidores" is a count.

    norm(text) -> str                         lower case, no accents
    parse_number("1.200") -> 1200.0           a plain number (digits or words)
    parse_money("R$ 5 mil") -> 5000.0         the value of a money expression
    find_money(norm(text)) -> [Money]         money spans in normalized text (start, end, value, bare)

The regex pieces (DIGITS_RE, WORDNUM_RE, MULT_RE, CUR_RE) are public so detectors can build on them.
"""

from __future__ import annotations

import re
import unicodedata
from dataclasses import dataclass

# ------------------------------------------------------------------ normalization

_PUNCT_MAP = {
    "‘": "'", "’": "'", "´": "'", "`": "'",
    "“": '"', "”": '"', "–": "-", "—": "-",
    " ": " ", " ": " ", "…": "...", "*": "",
}


def normalize(text: str) -> tuple[str, list[int]]:
    """Lowercase + strip accents. Returns (normalized, index map norm_pos -> original_pos)."""
    out: list[str] = []
    index: list[int] = []
    for i, ch in enumerate(text):
        mapped = _PUNCT_MAP.get(ch)
        pieces = mapped if mapped is not None else unicodedata.normalize("NFKD", ch)
        for piece in pieces:
            if unicodedata.combining(piece):
                continue
            for c in piece.lower():
                out.append(c)
                index.append(i)
    return "".join(out), index


def norm(text: str) -> str:
    return normalize(text)[0]


# ------------------------------------------------------------------ numbers & money

WORD_NUMBERS = {
    "um": 1, "uma": 1, "dois": 2, "duas": 2, "tres": 3, "quatro": 4, "cinco": 5, "seis": 6,
    "sete": 7, "oito": 8, "nove": 9, "dez": 10, "onze": 11, "doze": 12, "treze": 13,
    "quatorze": 14, "catorze": 14, "quinze": 15, "dezesseis": 16, "dezessete": 17,
    "dezoito": 18, "dezenove": 19, "vinte": 20, "trinta": 30, "quarenta": 40,
    "cinquenta": 50, "sessenta": 60, "setenta": 70, "oitenta": 80, "noventa": 90,
    "cem": 100, "cento": 100, "duzentos": 200, "duzentas": 200, "trezentos": 300,
    "quatrocentos": 400, "quinhentos": 500, "seiscentos": 600, "setecentos": 700,
    "oitocentos": 800, "novecentos": 900, "meio": 0.5, "meia": 0.5,
}
# "milão" is spoken slang for one million ("tá fazendo milão")
MULTIPLIERS = {"mil": 1_000, "k": 1_000, "milhao": 1_000_000, "milhoes": 1_000_000, "milao": 1_000_000,
               "miloes": 1_000_000, "mi": 1_000_000, "bilhao": 1_000_000_000, "bilhoes": 1_000_000_000,
               "bi": 1_000_000_000}

_WORDNUM_ALT = "|".join(sorted(WORD_NUMBERS, key=len, reverse=True))
WORDNUM_RE = rf"(?:{_WORDNUM_ALT})(?:\s+e\s+(?:{_WORDNUM_ALT}))*"
DIGITS_RE = r"\d{1,3}(?:\.\d{3})+(?:,\d{1,2})?|\d+(?:[.,]\d{1,2})?"
NUM_RE = rf"(?:{DIGITS_RE}|\b{WORDNUM_RE})"
MULT_RE = r"(?:mil|k|milhao|milhoes|milao|miloes|mi|bilhao|bilhoes|bi)"
CUR_RE = r"(?:reais|real|contos?|pilas?|dolares|dolar|euros?|usd|brl)"
# spoken compound amounts: "dois mil e quinhentos", "um milhão e meio", "um milhão e 200 mil"
_COMPOUND_TAIL = rf"(?:\s+e\s+(?:meio|meia|(?:{WORDNUM_RE}|\d{{1,3}})(?:\s+mil\b)?))?"
# "seis dígitos de faturamento": the number of digits of an amount
DIGIT_COUNT_RE = r"(?:cinco|seis|sete|oito|nove|5|6|7|8|9)\s+digitos\b"

MONEY_RE = (
    rf"(?:(?:r\$|us\$|u\$|\$|€)\s*(?:{DIGITS_RE})(?:\s*{MULT_RE}\b)?(?:\s+(?:de\s+)?{CUR_RE}\b)?"
    rf"|(?<![\w$]){NUM_RE}\s*{MULT_RE}\b{_COMPOUND_TAIL}(?:\s+(?:de\s+)?{CUR_RE}\b)?"
    rf"|(?<![\w$]){NUM_RE}\s+(?:de\s+)?{CUR_RE}\b"
    rf"|\bmil\s+e\s+(?:{WORDNUM_RE}|\d{{1,3}})\b(?:\s+(?:de\s+)?{CUR_RE}\b)?"
    rf"|\bmil\s+(?:de\s+)?{CUR_RE}\b"
    rf"|(?<![\w$]){DIGIT_COUNT_RE}"
    rf"|\b(?:mil\s+)?milh?(?:ao|oes)\b{_COMPOUND_TAIL}(?:\s+(?:de\s+)?{CUR_RE}\b)?)"
)
_MONEY_COMPILED = re.compile(MONEY_RE)

# Nouns that make a bare "10 mil" a count, not money.
NON_MONEY_NOUNS = re.compile(
    r"\s*(?:de\s+)?(?:seguidor\w*|inscrit\w*|visualiza\w*|views?|curtidas?|likes?|pessoas?|alun\w*|"
    r"clientes?|leads?|vendas?|pedidos?|unidades?|produtos?|pecas?|metros?|km|quilos?|kg|horas?|"
    r"minutos?|dias?|anos?|mensage\w*|comentarios?|compartilhament\w*|downloads?|acessos?|cliques?|"
    r"impress\w*|membros?|usuari\w*|contatos?|numeros?|videos?|posts?|lojas?|fornecedor\w*|itens|"
    r"copias?|livros?|pacotes?|caixas?|vezes|passos|reels|stories|grupos?|mulheres|homens|familias?)\b"
)


@dataclass
class Money:
    start: int
    end: int
    value: float | None
    bare: bool  # no currency marker (R$, reais...): needs context to count as money


def parse_number(token: str) -> float | None:
    token = token.strip()
    if not token:
        return None
    if re.fullmatch(r"\d{1,3}(?:\.\d{3})+(?:,\d{1,2})?", token):
        return float(token.replace(".", "").replace(",", "."))
    if re.fullmatch(r"\d+(?:[.,]\d{1,2})?", token):
        return float(token.replace(",", "."))
    words = [w for w in re.split(r"\s+e\s+|\s+", token) if w]
    if words and all(w in WORD_NUMBERS for w in words):
        return float(sum(WORD_NUMBERS[w] for w in words))
    return None


def _parse_compound(t: str) -> float | None:
    """'dois mil e quinhentos' -> 2500, 'mil e quinhentos' -> 1500, 'um milhão e meio' -> 1.5e6."""
    m = re.fullmatch(r"(?:(.+?)\s+)?(mil|milhao|milhoes|milao|miloes)\s+e\s+(meio|meia|.+?)(?:\s+(mil))?", t)
    if not m:
        return None
    unit = float(MULTIPLIERS[m.group(2)])
    head = parse_number(m.group(1)) if m.group(1) else 1.0
    if head is None:
        return None
    if m.group(3) in ("meio", "meia"):
        return head * unit + unit / 2
    tail = parse_number(m.group(3))
    if tail is None:
        return None
    tail *= 1000.0 if m.group(4) else 1.0
    return head * unit + tail if tail < unit else None


def parse_money(text: str) -> float | None:
    """Value in currency units of a money expression. '5 mil' -> 5000, 'R$ 1.500,00' -> 1500."""
    t = norm(text)
    t = re.sub(r"(?:r\$|us\$|u\$|\$|€)", " ", t)
    t = re.sub(rf"\b(?:de\s+)?{CUR_RE}\b", " ", t).strip()
    digits = re.fullmatch(DIGIT_COUNT_RE, t)
    if digits:
        n = WORD_NUMBERS.get(t.split()[0]) or float(t.split()[0])
        return float(10 ** (int(n) - 1))
    compound = _parse_compound(" ".join(t.split()))
    if compound is not None:
        return compound
    mult = 1.0
    m = re.search(rf"({MULT_RE})\s*$", t)
    if m and (m.start(1) == 0 or not t[m.start(1) - 1].isalpha()):
        mult = float(MULTIPLIERS[m.group(1)])
        t = t[: m.start(1)].strip()
    if not t:
        return mult if mult > 1 else None
    if t == "mil":  # "mil reais" -> 1000 · "mil milhão" (speech slip) -> 1 milhão
        return 1_000.0 if mult == 1.0 else mult
    base = parse_number(t)
    if base is None:
        return None
    return base * mult


def find_money(ntext: str) -> list[Money]:
    """Money spans in normalized text."""
    found: list[Money] = []
    for m in _MONEY_COMPILED.finditer(ntext):
        s = m.group(0).rstrip()
        start, end = m.start(), m.start() + len(s)
        bare = not re.search(rf"(?:r\$|\$|€|\b{CUR_RE}\b)", s)
        if re.search(r"digitos$", s):
            if re.search(r"\b(?:senha|codigo|numero|pin|token|cep|cpf|cnpj|telefone|celular|chave)\s+(?:\w+\s+)?(?:de|com)\s+$",
                         ntext[max(0, start - 30):start]):
                continue  # "senha de 6 dígitos"
            bare = False
        counted = NON_MONEY_NOUNS.match(ntext, end) if bare else None
        if counted and not re.match(r"\s+(?:passad[oa]|retrasad[oa]|atras)\b", ntext[counted.end():]):
            continue  # "10 mil seguidores" is a count ("1 milhão ano passado" is still money)
        if bare and re.fullmatch(r"\d+\s*(?:mi|bi)", s):  # "5 mi" is too ambiguous alone
            continue
        if s in ("milao", "miloes") and re.search(r"\b(?:em|de|para|pra|pro|a|ao|da|do|na|no|via|ate)\s+$",
                                                  ntext[max(0, start - 8):start]):
            continue  # "fui pra Milão": the city, not the slang
        found.append(Money(start, end, parse_money(s), bare))
    return found
