import re
from typing import Callable, Any, Iterable

from .exceptions import try_until_ok
from .patterns import INT, ANY
from .validators import is_in_list, is_in_range


def valid_input(prompt: str = '',
                pattern: str | re.Pattern = ANY,
                validator: Callable[[str], bool] = lambda x: True,
                converter: Callable[[str], Any] = str
                ) -> Any:
    """
    Prompts for input from the user, checks it against a regex pattern and a custom validator, and returns the converted value.

    The function enforces a strict validation sequence: Pattern Check -> Validator Check -> Conversion.

    :param prompt: Text to display before input. Defaults to an empty string.
    :param pattern: regex (str or re.Pattern) to match against the input text.
                    Defaults to re.compile(r'.*'), which always matches anything.
    :param validator: Callable[[str], bool] to validate the input text. Defaults to always return True.
    :param converter: Callable[[str], Any] to convert the input text. Defaults to str.
    :return: The entered string (by default) or converted value. Type depends on the converter function used.
    """
    input_text = input(prompt)

    if isinstance(pattern, str):
        pattern = re.compile(pattern)

    if pattern.fullmatch(input_text) and validator(input_text):
        return converter(input_text)
    else:
        raise ValueError('invalid input format!')

def get_valid_input(prompt: str = '',
                    pattern: str | re.Pattern = ANY,
                    validator: Callable[[str], bool] = lambda x: True,
                    converter: Callable[[str], Any] = str,
                    if_incorrect: str = 'Incorrect input!'
                    ) -> Any:
    return try_until_ok(valid_input,
                        prompt=prompt,
                        pattern=pattern,
                        validator=validator,
                        converter=converter,
                        exceptions=ValueError,
                        on_exception=if_incorrect)

def choose_from_list(
        options: list[str],
        by_number: bool = False,
        case_sensitive: bool = True,
        prompt: str = 'Choose option: ',
        if_incorrect: str = 'Incorrect option!'
        ) -> int | str:
    """
    Prompts the user for a choice from a list of options.
    Returns the 0-based index of the chosen option or a chosen option.

    :param options: List of possible (strings).
    :param by_number: If true, prompts the user for a valid number, else prompts the user for a valid string.
    :param case_sensitive: If True, validation is case-sensitive;
                           if False, case is ignored during comparison.
    :param prompt: Text displayed to the user before input. Defaults to 'Choose option: '
    :param if_incorrect: Text displayed to the user if input is incorrect. Defaults to 'Incorrect option!'.
    :raises ValueError: If the options list is empty.
    :return: The 0-based index of the chosen option from the list or a chosen option.
    """
    if not options:
        raise ValueError("options list cannot be empty.")

    if by_number:
        return try_until_ok(valid_input,
                            exceptions=(ValueError, TypeError),
                            prompt=prompt,
                            pattern=INT,
                            validator=is_in_range(1, len(options)),
                            converter=lambda t: int(t)-1,
                            on_exception=if_incorrect)
    else:
        return try_until_ok(valid_input,
                            exceptions=(ValueError, TypeError),
                            prompt=prompt,
                            validator=is_in_list(options, case_sensitive),
                            on_exception=if_incorrect)

def print_iterable(iterable: Iterable[Any], item_pattern: str = '{}',
                   join_by: str = '\n', start: str = '', end: str = '') -> None:
    """
    Convenient formatted output of iterable objects.

    :param iterable: The iterable to be formatted.
    :param item_pattern: Pattern to format.
    :param join_by: Separator between elements.
    :param start: Start of the output string.
    :param end: End of the output string.
    :return: None
    """
    formated_items = [item_pattern.format(item) for item in iterable]
    print(start+join_by.join(formated_items)+end)

def print_zipped_iterable(iterable: Iterable[Iterable[Any]], item_pattern: str = '{}: {}',
                          join_by: str='\n', start: str = '', end: str = '') -> None:
    """
    Convenient formatted output of iterable objects that can be unpacked (e.g., tuples or lists).
    This is useful for items produced by zip(), dict.items(), etc.

    :param iterable: The iterable containing unpackable elements (e.g., [(1, 'a'), (2, 'b')]).
    :param item_pattern: Pattern to format.
    :param join_by: Separator between elements.
    :param start: Start of the output string.
    :param end: End of the output string.
    :return: None
    """
    formated_items = [item_pattern.format(*item) for item in iterable]
    print(start+join_by.join(formated_items)+end)

def print_header(header: str, symbol: str='~') -> None:
    print(symbol*len(header) + '\n' + header + '\n' + symbol*len(header))
