"""Voice mode text normalization for Romanian TTS.

Pure functions — no side effects, no I/O, no logging. Strip markdown,
expand numbers / currency / symbols / abbreviations into natural-sounding
Romanian text. See plan: src/voice/normalize.py (Pas 3).

Pipeline order in normalize_for_tts:
    strip_markdown -> expand_abbreviations -> expand_currency
    -> expand_numbers_ro -> expand_symbols -> truncate(200)

Currency runs BEFORE generic number expansion so "12.50 RON" becomes
"doisprezece lei și cincizeci de bani" rather than
"doisprezece virgulă cincizeci RON".
"""
import re

from num2words import num2words


# ---------- Markdown ----------

_MARKDOWN_LINK = re.compile(r'\[([^\]]+)\]\([^)]+\)')
_MARKDOWN_BOLD = re.compile(r'\*\*([^*]+)\*\*')
_MARKDOWN_CODE = re.compile(r'`([^`\n]+)`')
_MARKDOWN_ITALIC = re.compile(r'(?<!\*)\*([^*\n]+)\*(?!\*)')
_MARKDOWN_HEADING = re.compile(r'^[ \t]*#{1,6}[ \t]+', re.MULTILINE)
_MARKDOWN_LIST = re.compile(r'^[ \t]*[-*+][ \t]+', re.MULTILINE)


def strip_markdown(text: str) -> str:
    """Remove common markdown formatting, preserve the visible content."""
    text = _MARKDOWN_LINK.sub(r'\1', text)
    text = _MARKDOWN_BOLD.sub(r'\1', text)
    text = _MARKDOWN_CODE.sub(r'\1', text)
    text = _MARKDOWN_ITALIC.sub(r'\1', text)
    text = _MARKDOWN_HEADING.sub('', text)
    text = _MARKDOWN_LIST.sub('', text)
    return text


# ---------- Number helpers ----------

def _needs_de(n: int) -> bool:
    """Romanian: insert 'de' between numeral and noun for n >= 20,
    except when the trailing 1-19 portion makes it sound off
    (e.g., 105, 119 -> no 'de'; 120, 200 -> 'de').
    """
    if n < 20:
        return False
    last = n % 100
    if 1 <= last <= 19:
        return False
    return True


def _int_to_ro(n: int) -> str:
    return num2words(n, lang='ro')


def _decimal_to_ro(s: str) -> str:
    """Convert decimal string 'X.Y' to RO words.

    Decimal part is read as a whole number ('3.14' -> 'trei virgulă paisprezece'),
    unless it has a leading zero ('3.05' -> 'trei virgulă zero cinci') so the
    magnitude is preserved.
    """
    int_part, dec_part = s.split('.', 1)
    int_words = _int_to_ro(int(int_part))
    if dec_part.startswith('0') and len(dec_part) > 1:
        dec_words = ' '.join(_int_to_ro(int(d)) for d in dec_part)
    else:
        dec_words = _int_to_ro(int(dec_part))
    return f"{int_words} virgulă {dec_words}"


# ---------- Numbers ----------

_NUM_TOKEN = re.compile(r'(?<!\w)(\d+(?:\.\d+)?)(?!\w)')


def expand_numbers_ro(text: str) -> str:
    """Expand bare numeric tokens to Romanian words.

    Only matches pure number tokens (no surrounding letters). Decimals
    use 'virgulă' separator. Currency-bound numbers should already be
    handled by expand_currency before this runs.
    """
    def _sub(match: re.Match) -> str:
        token = match.group(1)
        if '.' in token:
            return _decimal_to_ro(token)
        return _int_to_ro(int(token))

    return _NUM_TOKEN.sub(_sub, text)


# ---------- Thousands separator ----------

# Romanian uses dot or space as thousands separator: 384.000 / 384 000. The
# decimal expander would read "384.000" as "trei sute optzeci și patru virgulă
# zero zero zero" — wrong. Collapse the dots so expand_numbers_ro reads the
# whole integer.  Only 1-3 leading digits followed by ≥1 group of exactly 3
# digits, never adjacent to other digits.
_THOUSANDS_DOT = re.compile(r'(?<!\d)(\d{1,3}(?:\.\d{3})+)(?!\d)')


def normalize_thousands(text: str) -> str:
    """Strip the dot from Romanian thousands-separator integers."""
    return _THOUSANDS_DOT.sub(lambda m: m.group(1).replace('.', ''), text)


# ---------- Metric units ----------

# (regex_matching_<n><unit>, singular, plural). Matches an integer or decimal
# followed by the abbreviation as a whole word. Skipping bare ``m`` and ``l``
# because they collide with too many tokens ("M2" voice id, list markers).
_UNIT_PATTERNS: list[tuple[re.Pattern, str, str]] = [
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*km\b', re.IGNORECASE), 'kilometru', 'kilometri'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*kg\b', re.IGNORECASE), 'kilogram', 'kilograme'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*cm\b', re.IGNORECASE), 'centimetru', 'centimetri'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*mm\b', re.IGNORECASE), 'milimetru', 'milimetri'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*ml\b', re.IGNORECASE), 'mililitru', 'mililitri'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*ha\b', re.IGNORECASE), 'hectar', 'hectare'),
    (re.compile(r'(?<!\w)(\d+(?:[.,]\d+)?)\s*mp\b', re.IGNORECASE), 'metru pătrat', 'metri pătrați'),
]


def _format_unit(amount_str: str, singular: str, plural: str) -> str:
    """Mirror ``_format_currency_unit`` for metric units. Decimals fall through
    to the generic decimal expander (which leaves them with plural form)."""
    if '.' in amount_str or ',' in amount_str:
        return f"{_decimal_to_ro(amount_str.replace(',', '.'))} {plural}"
    return _format_currency_unit(int(amount_str), singular, plural)


def expand_units(text: str) -> str:
    """Expand metric unit abbreviations into spoken Romanian."""
    for pattern, singular, plural in _UNIT_PATTERNS:
        text = pattern.sub(
            lambda m, sg=singular, pl=plural: _format_unit(m.group(1), sg, pl),
            text,
        )
    return text


# ---------- Time ----------

_TIME_PATTERN = re.compile(r'(?<!\d)([01]?\d|2[0-3]):([0-5]?\d)(?!\d)')


def _format_minutes_ro(n: int) -> str:
    """Romanian-correct feminine forms for minute counts (0-59)."""
    if n == 1:
        return "un minut"
    if n == 2:
        return "două minute"
    if n < 20:
        return f"{_int_to_ro(n)} minute"
    last = n % 10
    rest = n - last
    if last == 0:
        return f"{_int_to_ro(n)} de minute"
    if last == 1:
        return f"{_int_to_ro(rest)} și una de minute"
    if last == 2:
        return f"{_int_to_ro(rest)} și două de minute"
    return f"{_int_to_ro(rest)} și {_int_to_ro(last)} de minute"


def expand_time(text: str) -> str:
    """Expand ``HH:MM`` clock times into colloquial Romanian.

    23:09 -> "douăzeci și trei și nouă minute"
    23:00 -> "douăzeci și trei fix"
    """
    def _sub(match: re.Match) -> str:
        h = int(match.group(1))
        m = int(match.group(2))
        hour_str = _int_to_ro(h)
        if m == 0:
            return f"{hour_str} fix"
        return f"{hour_str} și {_format_minutes_ro(m)}"

    return _TIME_PATTERN.sub(_sub, text)


# ---------- Currency ----------

_CURRENCY_MAIN = {
    'RON': ('leu', 'lei'),
    'USD': ('dolar', 'dolari'),
    'EUR': ('euro', 'euro'),
    'GBP': ('liră', 'lire'),
}

_CURRENCY_SUB = {
    'RON': ('ban', 'bani'),
    'USD': ('cent', 'cenți'),
    'EUR': ('cent', 'cenți'),
    'GBP': ('penny', 'pence'),
}

_CURRENCY_PATTERNS = [
    # RON suffix (case-insensitive: RON, ron, lei). Accepts either a
    # dot-decimal fraction ("12.50 lei") or, since Romanian accounting
    # format uses comma for decimals ("53.600,00 lei" — the dot-thousands
    # group is already collapsed by normalize_thousands upstream), a
    # comma-decimal fraction in its own capture group.
    (re.compile(r'(?<!\w)(\d+(?:\.\d+)?)(?:,(\d{1,2}))?\s+(?:RON|lei)\b', re.IGNORECASE), 'RON'),
    # Prefix currencies
    (re.compile(r'\$(\d+(?:\.\d+)?)'), 'USD'),
    (re.compile(r'€(\d+(?:\.\d+)?)'), 'EUR'),
    (re.compile(r'£(\d+(?:\.\d+)?)'), 'GBP'),
]


def _format_currency_unit(n: int, singular: str, plural: str) -> str:
    """Format integer amount + currency noun with proper RO singular/plural
    and 'de' particle. Uses 'un' (article) for n=1, not 'unu' (cardinal).
    """
    if n == 1:
        return f"un {singular}"
    word = _int_to_ro(n)
    if _needs_de(n):
        return f"{word} de {plural}"
    return f"{word} {plural}"


def _format_currency(amount: str, code: str, comma_frac: str | None = None) -> str:
    main_sg, main_pl = _CURRENCY_MAIN[code]
    if comma_frac is not None:
        whole_s, frac_s = amount, comma_frac
    elif '.' in amount:
        whole_s, frac_s = amount.split('.', 1)
    else:
        return _format_currency_unit(int(amount), main_sg, main_pl)
    # Normalize fractional part to 2 digits so "12.5 RON" reads as
    # 50 bani, not 5 bani.
    if len(frac_s) == 1:
        frac_s = frac_s + '0'
    elif len(frac_s) > 2:
        frac_s = frac_s[:2]
    whole = int(whole_s)
    frac = int(frac_s)
    whole_part = _format_currency_unit(whole, main_sg, main_pl)
    if frac == 0:
        return whole_part
    sub_sg, sub_pl = _CURRENCY_SUB[code]
    frac_part = _format_currency_unit(frac, sub_sg, sub_pl)
    return f"{whole_part} și {frac_part}"


def expand_currency(text: str) -> str:
    """Expand currency amounts into natural Romanian.

    Recognises ``<n> RON`` / ``<n> lei`` suffix and ``$``, ``€``, ``£`` prefix
    forms with optional 2-decimal fractional part (treated as sub-unit:
    bani / cenți / pence).
    """
    for pattern, code in _CURRENCY_PATTERNS:
        def _sub(m, c=code):
            comma_frac = m.group(2) if m.re.groups >= 2 else None
            return _format_currency(m.group(1), c, comma_frac)
        text = pattern.sub(_sub, text)
    return text


# ---------- Symbols ----------

def expand_symbols(text: str) -> str:
    """Replace common symbols with their Romanian spoken form."""
    text = text.replace('%', ' la sută')
    text = text.replace('&', ' și ')
    text = text.replace('@', ' la ')
    text = text.replace('°', ' grade')
    text = re.sub(r'\s+', ' ', text).strip()
    return text


from tools.tts import sanitize_for_supertonic as sanitize_punctuation


# ---------- Abbreviations ----------

# Longer patterns first so 'ș.a.m.d.' wins over 'ș.a.'
_ABBREVIATIONS = [
    (re.compile(r'(?<!\w)[șş]\.a\.m\.d\.', re.IGNORECASE), 'și așa mai departe'),
    (re.compile(r'(?<!\w)[șş]\.a\.', re.IGNORECASE), 'și altele'),
    (re.compile(r'(?<!\w)etc\.', re.IGNORECASE), 'etcetera'),
    (re.compile(r'(?<!\w)dl\.', re.IGNORECASE), 'domnul'),
    (re.compile(r'(?<!\w)dna\.', re.IGNORECASE), 'doamna'),
    (re.compile(r'(?<!\w)nr\.', re.IGNORECASE), 'numărul'),
]


def expand_abbreviations(text: str) -> str:
    """Expand Romanian abbreviations into their full forms."""
    for pattern, replacement in _ABBREVIATIONS:
        text = pattern.sub(replacement, text)
    return text


# ---------- Top-level pipeline ----------

_MAX_WORDS = 200
_TRUNCATE_SUFFIX = "Restul l-am scris în chat."


def expand_for_tts(text: str) -> str:
    """Apply the full normalization pipeline (markdown strip, abbreviations,
    numbers, currency, units, symbols) WITHOUT the 200-word truncation.

    Use this for one-shot TTS generation (e.g. /audio command) where the
    "Restul l-am scris în chat." suffix from normalize_for_tts() would be
    misleading (no live chat mirror exists for that flow).
    """
    text = strip_markdown(text)
    text = sanitize_punctuation(text)
    text = expand_abbreviations(text)
    text = normalize_thousands(text)
    text = expand_time(text)
    text = expand_currency(text)
    text = expand_units(text)
    text = expand_numbers_ro(text)
    text = expand_symbols(text)
    return text.strip()


def normalize_for_tts(text: str) -> str:
    """Apply the full normalization pipeline and truncate to 200 words.

    If the text exceeds 200 words, the first 200 are kept and the suffix
    "Restul l-am scris în chat." is appended so the listener knows the
    response continues in the text channel mirror.
    """
    text = expand_for_tts(text)
    words = text.split()
    if len(words) > _MAX_WORDS:
        text = ' '.join(words[:_MAX_WORDS]) + f" {_TRUNCATE_SUFFIX}"
    return text.strip()
