import re
from enum import Enum
from typing import List, Union, Dict, Tuple

from ovos_number_parser.util import Scale, GrammaticalGender, DigitPronunciation


class PortugueseVariant(str, Enum):
    """
    Defines the Portuguese variant for spelling.
    - BR: Brazilian Portuguese (e.g., bilhão, dezesseis).
    - PT: European Portuguese (e.g., bilião, dezasseis).
    """
    BR = "br"
    PT = "pt"


# --- Base Pronunciation Dictionaries (Variant-aware) ---
# Dictionaries for units, tens, and hundreds, separated by Portuguese variant.

_UNITS: Dict[int, str] = {
    1: 'um', 2: 'dois', 3: 'três', 4: 'quatro', 5: 'cinco', 6: 'seis',
    7: 'sete', 8: 'oito', 9: 'nove'
}

_TENS_BR: Dict[int, str] = {
    10: 'dez', 11: 'onze', 12: 'doze', 13: 'treze', 14: 'catorze',
    15: 'quinze', 16: 'dezesseis', 17: 'dezessete', 18: 'dezoito',
    19: 'dezenove', 20: 'vinte', 30: 'trinta', 40: 'quarenta',
    50: 'cinquenta', 60: 'sessenta', 70: 'setenta', 80: 'oitenta',
    90: 'noventa'
}

_TENS_PT: Dict[int, str] = {
    10: 'dez', 11: 'onze', 12: 'doze', 13: 'treze', 14: 'catorze',
    15: 'quinze', 16: 'dezasseis', 17: 'dezassete', 18: 'dezoito',
    19: 'dezanove', 20: 'vinte', 30: 'trinta', 40: 'quarenta',
    50: 'cinquenta', 60: 'sessenta', 70: 'setenta', 80: 'oitenta',
    90: 'noventa'
}

_HUNDREDS: Dict[int, str] = {
    100: 'cem', 200: 'duzentos', 300: 'trezentos', 400: 'quatrocentos',
    500: 'quinhentos', 600: 'seiscentos', 700: 'setecentos',
    800: 'oitocentos', 900: 'novecentos'
}

_FRACTION_STRING_PT: Dict[int, str] = {
    2: 'meio', 3: 'terço', 4: 'quarto', 5: 'quinto', 6: 'sexto',
    7: 'sétimo', 8: 'oitavo', 9: 'nono', 10: 'décimo',
    11: 'onze avos', 12: 'doze avos', 13: 'treze avos', 14: 'catorze avos',
    15: 'quinze avos', 16: 'dezasseis avos', 17: 'dezassete avos',
    18: 'dezoito avos', 19: 'dezanove avos',
    20: 'vigésimo', 30: 'trigésimo', 100: 'centésimo', 1000: 'milésimo'
}

_FEMALE_NUMS = {
    "uma": 1,
    "duas": 2
}

# --- Ordinal Pronunciation Dictionaries (Masculine Base) ---
# These dictionaries are for masculine forms. The feminine form
# is generated by replacing the final 'o' with 'a'.
_ORDINAL_UNITS_MASC: Dict[int, str] = {
    1: 'primeiro', 2: 'segundo', 3: 'terceiro', 4: 'quarto', 5: 'quinto',
    6: 'sexto', 7: 'sétimo', 8: 'oitavo', 9: 'nono'
}

_ORDINAL_TENS_MASC: Dict[int, str] = {
    10: 'décimo', 20: 'vigésimo', 30: 'trigésimo', 40: 'quadragésimo',
    50: 'quinquagésimo', 60: 'sexagésimo', 70: 'septuagésimo',
    80: 'octogésimo', 90: 'nonagésimo'
}

_ORDINAL_HUNDREDS_MASC: Dict[int, str] = {
    100: 'centésimo', 200: 'ducentésimo', 300: 'trecentésimo',
    400: 'quadrigentésimo', 500: 'quingentésimo', 600: 'sexcentésimo',
    700: 'septingentésimo', 800: 'octingentésimo', 900: 'noningentésimo'
}

_ORDINAL_SCALES_MASC: Dict[Scale, Dict[PortugueseVariant, List[Tuple[int, str]]]] = {
    Scale.SHORT: {
        PortugueseVariant.BR: [
            (10 ** 21, "sextilionésimo"),
            (10 ** 18, "quintilionésimo"),
            (10 ** 15, "quadrilionésimo"),
            (10 ** 12, "trilionésimo"),
            (10 ** 9, "bilionésimo"),
            (10 ** 6, "milionésimo"),
            (10 ** 3, "milésimo")
        ],
        PortugueseVariant.PT: [
            (10 ** 21, "sextilionésimo"),
            (10 ** 18, "quintilionésimo"),
            (10 ** 15, "quatrilionésimo"),
            (10 ** 12, "trilionésimo"),
            (10 ** 9, "bilionésimo"),
            (10 ** 6, "milionésimo"),
            (10 ** 3, "milésimo")
        ]
    },
    Scale.LONG: {
        PortugueseVariant.BR: [
            (10 ** 36, "sextilionésimo"),
            (10 ** 30, "quintilionésimo"),
            (10 ** 24, "quatrilionésimo"),
            (10 ** 18, "trilionésimo"),
            (10 ** 12, "bilionésimo"),
            (10 ** 6, "milionésimo"),
            (10 ** 3, "milésimo")
        ],
        PortugueseVariant.PT: [
            (10 ** 36, "sextilionésimo"),
            (10 ** 30, "quintilionésimo"),
            (10 ** 24, "quatrilionésimo"),
            (10 ** 18, "trilionésimo"),
            (10 ** 12, "bilionésimo"),
            (10 ** 6, "milionésimo"),
            (10 ** 3, "milésimo")
        ]
    }
}


# --- Scale Definitions ---
# Structure: (value, singular_name, plural_name)
# Ordered from largest to smallest.
_SCALES: Dict[Scale, Dict[PortugueseVariant, List[Tuple[int, str, str]]]] = {
    Scale.SHORT: {
        PortugueseVariant.BR: [
            (10 ** 21, "sextilhão", "sextilhões"),
            (10 ** 18, "quintilhão", "quintilhões"),
            (10 ** 15, "quadrilhão", "quadrilhões"),
            (10 ** 12, "trilhão", "trilhões"),
            (10 ** 9, "bilhão", "bilhões"),
            (10 ** 6, "milhão", "milhões"),
            (10 ** 3, "mil", "mil")
        ],
        PortugueseVariant.PT: [
            (10 ** 21, "sextilião", "sextiliões"),
            (10 ** 18, "quintilião", "quintiliões"),
            (10 ** 15, "quatrilião", "quatriliões"),
            (10 ** 12, "trilião", "triliões"),
            (10 ** 9, "bilião", "biliões"),
            (10 ** 6, "milhão", "milhões"),
            (10 ** 3, "mil", "mil")
        ]
    },
    Scale.LONG: {
        PortugueseVariant.BR: [
            (10 ** 36, "sextilhão", "sextilhões"),
            (10 ** 30, "quintilhão", "quintilhões"),
            (10 ** 24, "quatrilhão", "quatrilhões"),
            (10 ** 18, "trilhão", "trilhões"),
            (10 ** 12, "bilhão", "bilhões"),
            (10 ** 6, "milhão", "milhões"),
            (10 ** 3, "mil", "mil")
        ],
        PortugueseVariant.PT: [
            (10 ** 36, "sextilião", "sextiliões"),
            (10 ** 30, "quintilião", "quintiliões"),
            (10 ** 24, "quatrilião", "quatriliões"),
            (10 ** 18, "trilião", "triliões"),
            (10 ** 12, "bilião", "biliões"),
            (10 ** 6, "milhão", "milhões"),
            (10 ** 3, "mil", "mil")
        ]
    }
}

# Mapping of number words to their integer values. This is dynamically built
# from the base dictionaries to ensure consistency and variant support.
_NUMBERS_BASE = {
    **_FEMALE_NUMS,
    **{v: k for k, v in _UNITS.items()},
    **{v: k for k, v in _TENS_PT.items()},
    **{v: k for k, v in _TENS_BR.items()},
    **{v: k for k, v in _HUNDREDS.items()},
    "cento": 100
}

def get_number_map(scale: Scale = Scale.LONG,
                   variant: PortugueseVariant = PortugueseVariant.PT):
    """
    Return a dictionary mapping Portuguese number words, including scale names, to their integer values for the specified scale and language variant.

    Parameters:
        scale (Scale): The numerical scale to use (short or long).
        variant (PortugueseVariant): The Portuguese language variant (Brazilian or European).

    Returns:
        dict: Mapping of Portuguese number words (units, tens, hundreds, scale names) to their corresponding integer values.
    """
    return {
        **_NUMBERS_BASE,
        **{s_name: val for val, s_name, _ in _SCALES[scale][variant]},
        **{p_name: val for val, _, p_name in _SCALES[scale][variant]}
    }

_NUMBERS_BR = get_number_map(Scale.SHORT, PortugueseVariant.BR)
_NUMBERS_PT = get_number_map(Scale.LONG, PortugueseVariant.PT)

_ORDINAL_WORDS_MASC = {
    **{v: k for k, v in _ORDINAL_UNITS_MASC.items()},
    **{v: k for k, v in _ORDINAL_TENS_MASC.items()},
    **{v: k for k, v in _ORDINAL_HUNDREDS_MASC.items()},
    **{s_name: val for val, s_name in _ORDINAL_SCALES_MASC[Scale.SHORT][PortugueseVariant.BR]},
}


def _swap_gender(word: str, gender: GrammaticalGender) -> str:
    """
    Convert a Portuguese word between masculine and feminine grammatical gender by adjusting its ending.

    Parameters:
        word (str): The word to convert.
        gender (GrammaticalGender): The target grammatical gender.

    Returns:
        str: The word with its ending swapped to match the specified gender, if applicable; otherwise, the original word.
    """
    if gender == GrammaticalGender.FEMININE and word.endswith('o'):
        return word[:-1] + 'a'
    elif gender == GrammaticalGender.MASCULINE and word.endswith('a'):
        return word[:-1] + 'o'
    elif gender == GrammaticalGender.FEMININE and word.endswith('os'):
        return word[:-2] + 'as'
    elif gender == GrammaticalGender.MASCULINE and word.endswith('as'):
        return word[:-2] + 'os'
    return word


def _pronounce_up_to_999(
        n: int,
        variant: PortugueseVariant = PortugueseVariant.BR
) -> str:
    """
    Returns the Portuguese cardinal pronunciation of an integer from 0 to 999, using the specified language variant.

    Parameters:
        n (int): Integer to pronounce (must be between 0 and 999).
        variant (PortugueseVariant, optional): Portuguese variant (Brazilian or European). Defaults to Brazilian.

    Returns:
        str: The number pronounced in Portuguese words.

    Raises:
        ValueError: If n is not in the range 0 to 999.
    """
    if not 0 <= n <= 999:
        raise ValueError("Number must be between 0 and 999.")
    if n == 0:
        return "zero"
    if n == 100:
        return "cem"

    parts = []
    tens_map = _TENS_BR if variant == PortugueseVariant.BR else _TENS_PT

    # Hundreds
    if n >= 100:
        hundred = n // 100 * 100
        parts.append("cento" if hundred == 100 else _HUNDREDS[hundred])
        n %= 100
        if n > 0:
            parts.append("e")

    # Tens and Units
    if n > 0:
        if n < 20:
            parts.append(tens_map.get(n) or _UNITS.get(n, ""))
        else:
            ten = n // 10 * 10
            unit = n % 10
            parts.append(tens_map[ten])
            if unit > 0:
                parts.append("e")
                parts.append(_UNITS[unit])

    return " ".join(parts)


def _pronounce_ordinal_up_to_999(
        n: int,
        gender: GrammaticalGender = GrammaticalGender.MASCULINE,
        variant: PortugueseVariant = PortugueseVariant.PT
) -> str:
    """
    Returns the Portuguese ordinal word for an integer between 0 and 999, adjusting for grammatical gender and language variant.

    Parameters:
        n (int): The integer to convert (must be between 0 and 999).

    Returns:
        str: The ordinal representation of the number in Portuguese.

    Raises:
        ValueError: If n is not between 0 and 999.
    """
    if not 0 <= n <= 999:
        raise ValueError("Number must be between 0 and 999.")
    if n == 0:
        return "zero"

    parts = []

    # Handle hundreds
    if n >= 100:
        hundred_val = n // 100 * 100
        hundred_word_masc = _ORDINAL_HUNDREDS_MASC.get(hundred_val)
        if hundred_word_masc:
            parts.append(_swap_gender(hundred_word_masc, gender))
        n %= 100

    # Handle tens and units
    if n > 0:
        # Ordinal numbers don't use 'e' as a separator
        if n % 10 == 0 and n > 10:
            tens_word_masc = _ORDINAL_TENS_MASC[n]
            parts.append(_swap_gender(tens_word_masc, gender))
        elif n < 10:
            units_word_masc = _ORDINAL_UNITS_MASC[n]
            parts.append(_swap_gender(units_word_masc, gender))
        elif n < 20:
            tens_word_masc = _ORDINAL_TENS_MASC[10]
            units_word_masc = _ORDINAL_UNITS_MASC[n - 10]
            parts.append(f"{_swap_gender(tens_word_masc, gender)} {_swap_gender(units_word_masc, gender)}")
        else:
            tens_word_masc = _ORDINAL_TENS_MASC[n // 10 * 10]
            units_word_masc = _ORDINAL_UNITS_MASC[n % 10]
            parts.append(f"{_swap_gender(tens_word_masc, gender)} {_swap_gender(units_word_masc, gender)}")

    return " ".join(parts)


def pronounce_ordinal_pt(
        number: Union[int, float],
        gender: GrammaticalGender = GrammaticalGender.MASCULINE,
        scale: Scale = Scale.LONG,
        variant: PortugueseVariant = PortugueseVariant.PT
) -> str:
    """
    Return the ordinal pronunciation of a number in Portuguese, supporting grammatical gender, scale (short or long), and language variant (Brazilian or European Portuguese).

    Parameters:
        number (int or float): The number to pronounce as an ordinal.
        gender (GrammaticalGender, optional): The grammatical gender for the ordinal form (masculine or feminine).
        scale (Scale, optional): The numerical scale to use (short or long).
        variant (PortugueseVariant, optional): The Portuguese variant (Brazilian or European).

    Returns:
        str: The ordinal pronunciation of the number in Portuguese.

    Raises:
        TypeError: If `number` is not an int or float.
    """
    if not isinstance(number, (int, float)):
        raise TypeError("Number must be an int or float.")
    if number == 0:
        return "zero"

    if number < 0:
        return f"menos {pronounce_ordinal_pt(abs(number), gender, scale, variant)}"

    n = int(number)
    if n < 1000:
        return _pronounce_ordinal_up_to_999(n, gender, variant)

    ordinal_scale_defs = _ORDINAL_SCALES_MASC[scale][variant]

    # Find the largest scale that fits the number
    for scale_val, s_name in ordinal_scale_defs:
        if n >= scale_val:
            break

    count = n // scale_val
    remainder = n % scale_val

    # Special case for "milésimo" and other large scales where 'um' is not needed
    if count == 1 and scale_val >= 1000:
        count_str = _swap_gender(s_name, gender)
    else:
        # Pronounce the 'count' part of the number and the scale word
        count_pronunciation = pronounce_number_pt(count, scale=scale, variant=variant)
        scale_word_masc = s_name
        scale_word = _swap_gender(scale_word_masc, gender)
        count_str = f"{count_pronunciation} {scale_word}"

    # If there's no remainder, we're done
    if remainder == 0:
        return count_str

    # Pronounce the remainder and join
    remainder_str = pronounce_ordinal_pt(remainder, gender, scale, variant)

    return f"{count_str} {remainder_str}"


def is_fractional_pt(
        input_str: str
) -> Union[float, bool]:
    """
    Checks if the input string corresponds to a recognized Portuguese fractional word.

    Returns:
        The fractional value as a float if recognized (e.g., 0.5 for "meio" or "meia"); otherwise, False.
    """
    input_str = input_str.lower().strip()
    fraction_map = _FRACTION_STRING_PT

    # Handle plural forms
    if input_str.endswith('s') and input_str not in fraction_map.values():
        input_str = input_str[:-1]

    # Handle "meio" vs "meia"
    if input_str == "meia":
        input_str = "meio"

    # Use a dynamic lookup instead of a hardcoded list
    for den, word in fraction_map.items():
        # Handle cases like "onze avos", so we check for the whole word
        if input_str == word:
            return 1.0 / den

    # Special case for "meia" as a female form of "meio" (1/2)
    if input_str in ["meia", "meio"]:
        return 0.5

    return False


def is_ordinal_pt(input_str: str) -> bool:
    """
    Determine if a string is a Portuguese ordinal word.

    Returns:
        bool: True if the input string is recognized as a Portuguese ordinal, otherwise False.
    """
    input_str = _swap_gender(input_str, GrammaticalGender.MASCULINE)
    return input_str in _ORDINAL_WORDS_MASC


def extract_number_pt(
        text: str,
        ordinals: bool = False,
        scale: Scale = Scale.LONG,
        variant: PortugueseVariant = PortugueseVariant.PT
) -> Union[int, float, bool]:
    """
    Extracts a numeric value from a Portuguese text phrase, supporting cardinals, ordinals, fractions, and large scales.

    Parameters:
        text (str): The input phrase potentially containing a number.
        ordinals (bool): If True, recognizes ordinal words as numbers.
        scale (Scale): Specifies whether to use the short or long numerical scale.
        variant (PortugueseVariant): Specifies the Portuguese language variant (BR or PT).

    Returns:
        int or float: The extracted number if found; otherwise, False.
    """
    numbers_map = get_number_map(scale, variant)
    scales_map = _SCALES[scale][variant]

    clean_text = text.lower().replace('-', ' ')
    tokens = [t for t in clean_text.split() if t != "e"]

    result = 0
    current_number = 0
    number_consumed = False

    for i, token in enumerate(tokens):
        if token is None:
            continue # consumed in previous idx
        next_token = tokens[i+1] if i < len(tokens) - 1 else None
        next_digit = numbers_map.get(next_token) if next_token else None
        val = numbers_map.get(token)
        if val is not None:
            if next_digit and  next_digit > val:
                tokens[i+1] = None
                current_number += val * next_digit
            else:
                current_number += val
        elif ordinals and is_ordinal_pt(token):
            token = _swap_gender(token, GrammaticalGender.MASCULINE)
            current_number += _ORDINAL_WORDS_MASC[token]
        elif is_fractional_pt(token):
            fraction = is_fractional_pt(token)
            result += current_number + fraction
            current_number = 0
            number_consumed = True
        else:
            # Handle large scales like milhão, bilhão
            found_scale = False
            for scale_val, singular, plural in scales_map:
                if token == singular or token == plural:
                    if current_number == 0:
                        current_number = 1
                    result += current_number * scale_val
                    current_number = 0
                    found_scale = True
                    number_consumed = True
                    break
            if not found_scale:
                if token in ["ponto", "virgula", "vírgula", ".", ","]:
                    decimal_str = ''.join(
                        str(numbers_map.get(t, '')) for t in tokens[i+1:]
                        if t in numbers_map
                    )
                    if decimal_str:
                        result += current_number + float(f"0.{decimal_str}")
                        number_consumed = True
                    current_number = 0
                    break

    if not number_consumed:
        result += current_number

    return result if result > 0 else False



def pronounce_number_pt(
        number: Union[int, float],
        places: int = 5,
        scale: Scale = Scale.LONG,
        variant: PortugueseVariant = PortugueseVariant.PT,
        ordinals: bool = False,
        digits: DigitPronunciation = DigitPronunciation.FULL_NUMBER,
        gender: GrammaticalGender = GrammaticalGender.MASCULINE
) -> str:
    """
    Return the full Portuguese pronunciation of a number, supporting cardinal and ordinal forms, decimals, grammatical gender, and both Brazilian and European Portuguese variants.
    
    Parameters:
        number (int or float): The number to pronounce.
        places (int): Number of decimal places to include for floats.
        scale (Scale): Numerical scale to use (short or long).
        variant (PortugueseVariant): Portuguese language variant for pronunciation.
        ordinals (bool): If True, pronounce as an ordinal number.
        digits (DigitPronunciation): Determines whether decimal parts are pronounced as a whole number or digit by digit.
        gender (GrammaticalGender): Grammatical gender for ordinal numbers.
    
    Returns:
        str: The number expressed as a Portuguese phrase.
    """
    if not isinstance(number, (int, float)):
        raise TypeError("Number must be an int or float.")

    if ordinals:
        return pronounce_ordinal_pt(number, gender, scale, variant)

    if number == 0:
        return "zero"

    if number < 0:
        return f"menos {pronounce_number_pt(abs(number), places, scale, variant)}"

    # Handle decimals
    if isinstance(number, float):
        integer_part = int(number)
        decimal_part_str = f"{number:.{places}f}".split('.')[1].rstrip("0")

        # Handle cases where the decimal part rounds to zero
        if decimal_part_str and int(decimal_part_str) == 0:
            return pronounce_number_pt(integer_part, places, scale, variant)

        int_pronunciation = pronounce_number_pt(integer_part, places, scale, variant)

        decimal_pronunciation_parts = []
        #  pronounce decimals either as a whole number or digit by digit
        if decimal_part_str:
            if digits == DigitPronunciation.FULL_NUMBER:
                decimal_pronunciation_parts.append(_pronounce_up_to_999(int(decimal_part_str[:3]), variant))
            else:
                for digit in decimal_part_str:
                    decimal_pronunciation_parts.append(_pronounce_up_to_999(int(digit), variant))

        decimal_pronunciation = " ".join(decimal_pronunciation_parts) or "zero"
        decimal_word = "vírgula"
        return f"{int_pronunciation} {decimal_word} {decimal_pronunciation}"

    # --- Integer Pronunciation Logic ---
    n = int(number)

    # Base case for recursion: numbers less than 1000
    if n < 1000:
        return _pronounce_up_to_999(n, variant)

    scale_definitions = _SCALES[scale][variant]

    # Find the largest scale that fits the number
    for scale_val, s_name, p_name in scale_definitions:
        if n >= scale_val:
            break

    count = n // scale_val
    remainder = n % scale_val

    # Pronounce the 'count' part of the number
    scale_word = s_name if count == 1 else p_name
    if count == 1 and scale_word == "mil":
        count_str = scale_word
    else:
        count_pronunciation = pronounce_number_pt(count, places, scale, variant)
        count_str = f"{count_pronunciation} {scale_word}"

    # If there's no remainder, we're done
    if remainder == 0:
        return count_str

    # Pronounce the remainder and join with the correct conjunction
    remainder_str = pronounce_number_pt(remainder, places, scale, variant)

    # Conjunction logic: add "e" if the remainder is the last group and is
    # less than 100 or a multiple of 100.
    if remainder < 100 or (remainder < 1000 and remainder % 100 == 0):
        return f"{count_str} e {remainder_str}"
    else:
        return f"{count_str} {remainder_str}"


def numbers_to_digits_pt(
        utterance: str,
        scale: Scale = Scale.LONG,
        variant: PortugueseVariant = PortugueseVariant.PT
) -> str:
    """
    Converts written Portuguese numbers in a text string to their digit equivalents, preserving all other text.

    Identifies spans of number words (including the joiner "e"), extracts their numeric values, and replaces them with digit strings. Non-number words and context are left unchanged.

    Parameters:
        utterance (str): Input text possibly containing written Portuguese numbers.
        scale (Scale, optional): Numerical scale (short or long) to interpret large numbers. Defaults to Scale.LONG.
        variant (PortugueseVariant, optional): Portuguese language variant (BR or PT). Defaults to PortugueseVariant.PT.

    Returns:
        str: The input text with written numbers replaced by their digit representations.
    """
    words = tokenize(utterance)
    output = []
    i = 0
    NUMBERS = get_number_map(scale, variant)
    while i < len(words):
        # Look for the start of a number span
        if words[i] in NUMBERS:
            # Start a new span
            number_span_words = []
            j = i
            # Continue the span as long as we find number words or the joiner 'e'
            while j < len(words) and (words[j] in NUMBERS or words[j] == "e"):
                number_span_words.append(words[j])
                j += 1

            # Form the phrase from the span and extract the number value
            phrase = " ".join(number_span_words)
            number_val = extract_number_pt(phrase, variant=variant)

            if number_val is not False:
                # If a valid number is found, add its digit representation to the output
                output.append(str(number_val))
                # Advance the main index 'i' past the entire span
                i = j
            else:
                # If the span doesn't form a valid number, treat the first word as non-numeric
                # and move to the next word. This handles cases like "e" at the beginning of a sentence.
                output.append(words[i])
                i += 1
        else:
            # If the current word is not a number word, add it to the output
            # and move to the next word
            output.append(words[i])
            i += 1

    return " ".join(output)


def tokenize(utterance: str) -> List[str]:
    """
    Splits a Portuguese text string into a list of tokens, separating words and punctuation.

    Returns:
        A list of tokens, where each token is a word or punctuation mark from the input string.
    """
    # Split things like 12%
    utterance = re.sub(r"([0-9]+)([\%])", r"\1 \2", utterance)
    # Split things like #1
    utterance = re.sub(r"(\#)([0-9]+\b)", r"\1 \2", utterance)
    # Split things like amo-te, but preserve numbers like 1-2
    utterance = re.sub(r"([a-zA-Z]+)(-)([a-zA-Z]+\b)", r"\1 \2 \3",
                       utterance)

    tokens = utterance.split()
    # Remove a trailing hyphen if it's the last token
    if tokens and tokens[-1] == '-':
        tokens = tokens[:-1]

    return tokens


def pronounce_fraction_pt(word: str,
                          scale: Scale = Scale.LONG,
                          variant: PortugueseVariant = PortugueseVariant.PT) -> str:
    """
    Return the Portuguese pronunciation of a fraction given as a string (e.g., "1/2").

    The numerator is pronounced as a cardinal number, and the denominator as an ordinal or fraction name, pluralized if appropriate. For denominators not in the known fraction list, the denominator is pronounced as a cardinal number followed by "avos" if plural.

    Parameters:
        word (str): Fraction in the form "numerator/denominator" (e.g., "3/4").

    Returns:
        str: The Portuguese pronunciation of the fraction.
    """
    n1, n2 = word.split("/")
    n1_int, n2_int = int(n1), int(n2)

    # Pronounce the denominator (second number) as an ordinal, and pluralize it if needed.
    if n2_int in _FRACTION_STRING_PT:
        denom = _FRACTION_STRING_PT[n2_int]
        if n1_int != 1:
            denom += "s" # plural
    else:
        # For other numbers
        denom = pronounce_number_pt(n2_int, scale=scale, variant=variant)
        if n1_int > 1:  # plural
            denom += " avos"

    # Pronounce the numerator (first number) as a cardinal.
    num = pronounce_number_pt(n1_int, scale=scale, variant=variant)
    return f"{num} {denom}"


if __name__ == "__main__":
    print("--- Testing Pronunciation (Short Scale, BR Variant) ---")
    print(f"1,234,567: {pronounce_number_pt(1_234_567, scale=Scale.SHORT, variant=PortugueseVariant.BR)}")
    print(f"1,000,000,000: {pronounce_number_pt(1_000_000_000, scale=Scale.SHORT, variant=PortugueseVariant.BR)}")
    print(f"16: {pronounce_number_pt(16, variant=PortugueseVariant.BR)}")

    print("\n--- Testing Pronunciation (Long Scale, PT Variant) ---")
    print(f"1,000,000: {pronounce_number_pt(1_000_000, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"1,000,100: {pronounce_number_pt(1_000_100, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"1,000,000,000: {pronounce_number_pt(1_000_000_000, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"1,000,000,000,000: {pronounce_number_pt(1_000_000_000_000, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"2,500,000,000: {pronounce_number_pt(2_500_000_000, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"2,500,123,456: {pronounce_number_pt(2_500_123_456, scale=Scale.LONG, variant=PortugueseVariant.PT)}")
    print(f"16: {pronounce_number_pt(16, variant=PortugueseVariant.PT)}")

    print("\n--- Testing Edge Cases ---")
    print(f"-123.45: {pronounce_number_pt(-123.45)}")
    print(f"10.05: {pronounce_number_pt(10.05)}")
    print(f"2000: {pronounce_number_pt(2000)}")
    print(f"2001: {pronounce_number_pt(2001)}")
    print(f"123.456789: {pronounce_number_pt(123.456789)}")

    print("\n--- Testing Ordinal Pronunciation ---")
    print(f"1st (masculine): {pronounce_number_pt(1, ordinals=True, gender=GrammaticalGender.MASCULINE)}")
    print(f"1st (feminine): {pronounce_number_pt(1, ordinals=True, gender=GrammaticalGender.FEMININE)}")
    print(f"23rd (masculine): {pronounce_number_pt(23, ordinals=True)}")
    print(f"23rd (feminine): {pronounce_number_pt(23, ordinals=True, gender=GrammaticalGender.FEMININE)}")
    print(f"100th: {pronounce_number_pt(100, ordinals=True)}")
    print(f"101st: {pronounce_number_pt(101, ordinals=True)}")
    print(f"1000th: {pronounce_number_pt(1000, ordinals=True)}")
    print(f"1,000,000th: {pronounce_number_pt(1_000_000, ordinals=True)}")
    print(f"1,000,000,000,000th (PT, long): {pronounce_number_pt(1_000_000_000_000, ordinals=True, variant=PortugueseVariant.PT, scale=Scale.LONG)}")

    print("\n--- Testing numbers_to_digits_pt (BR) ---")
    print(f"'quinhentos e cinquenta' -> '{numbers_to_digits_pt('quinhentos e cinquenta')}'")
    print(f"'um milhão' -> '{numbers_to_digits_pt('um milhão')}'")
    print(f"'dezesseis' -> '{numbers_to_digits_pt('dezesseis')}'")
    print(f"'há duzentos e cinquenta carros' -> '{numbers_to_digits_pt('há duzentos e cinquenta carros')}'")

    print("\n--- Testing numbers_to_digits_pt (PT) ---")
    print(f"'quinhentos e cinquenta' -> '{numbers_to_digits_pt('quinhentos e cinquenta', variant=PortugueseVariant.PT)}'")
    print(f"'um milhão' -> '{numbers_to_digits_pt('um milhão', variant=PortugueseVariant.PT)}'")
    print(f"'dezasseis' -> '{numbers_to_digits_pt('dezasseis', variant=PortugueseVariant.PT)}'")

    print("\n--- Testing Ordinal Extraction ---")
    print(f"'o segundo carro' -> {extract_number_pt('o segundo carro', ordinals=True)}")
    print(f"'primeiro lugar' -> {extract_number_pt('primeiro lugar', ordinals=True)}")
    print(f"'o milésimo dia' -> {extract_number_pt('o milésimo dia', ordinals=True)}")
    print(f"'a milésima vez' -> {extract_number_pt('a milésima vez', ordinals=True)}")
    print(f"'a primeira vez' -> {extract_number_pt('a primeira vez', ordinals=True)}")
    print(f"'a sexagésima quarta vez' -> {extract_number_pt('a sexagésima quarta vez', ordinals=True)}")

    print("\n--- Testing Cardinal Extraction ---")
    print(f"'um' -> {extract_number_pt('um')}")
    print(f"'uma' -> {extract_number_pt('uma')}")
    print(f"'vinte e um' -> {extract_number_pt('vinte e um')}")
    print(f"'um milhão' -> {extract_number_pt('um milhão')}")
    print(f"'dois milhões e quinhentos' -> {extract_number_pt('dois milhões e quinhentos')}")
    print(f"'mil e vinte e três' -> {extract_number_pt('mil e vinte e três')}")
    print(f"'trinta e cinco vírgula quatro' -> {extract_number_pt('trinta e cinco vírgula quatro')}")


    print("\n--- Testing Fractions ---")
    print(f"1/2: {pronounce_fraction_pt('1/2')}")
    print(f"2/2: {pronounce_fraction_pt('2/2')}")
    print(f"5/2: {pronounce_fraction_pt('5/2')}")
    print(f"5/3: {pronounce_fraction_pt('5/3')}")
    print(f"5/4: {pronounce_fraction_pt('5/4')}")
    print(f"7/5: {pronounce_fraction_pt('7/5')}")
    print(f"0/20: {pronounce_fraction_pt('0/20')}")

