import re
from typing import Callable, Any, Iterable

from .exceptions import ValidationError, ConversionError, APIError
from .patterns import INT, ANY
from .validators import is_in_list, is_in_range
from .utils import format_iterable, format_zipped_iterable, try_until_ok


def valid_input(prompt: str = '',
                pattern: str | re.Pattern = ANY,
                validator: Callable[[str], bool] = lambda x: True,
                converter: Callable[[str], Any] = str
                ) -> Any:
    """
    Prompts the user for input and performs a single-pass validation check.

    This is the core, non-looping validation function. It enforces a strict
    sequence: 1) RegEx Pattern Check, 2) Custom Validator Check, 3) Conversion.

    Args:
        prompt: The message displayed to the user.
        pattern: A regex pattern (str or re.Pattern) the input must fully match.
        validator: A custom function (Callable[[str], bool]) for additional checks.
        converter: A function (Callable[[str], Any]) to convert the input string
                   to the desired type (e.g., int, float).

    Returns:
        The converted input value (Any).

    Raises:
        InputValidationError: If the input does not match the 'pattern' or fails
                              the 'validator' check.
        ConversionError: If the 'converter' function raises an exception
                         (e.g., ValueError, TypeError) during type conversion.
                         This is a subclass of InputValidationError.

    Note:
        For interactive use with automatic retries and custom error messages,
        use 'get_valid_input()' which wraps this function.
    """
    input_text = input(prompt)

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

    if pattern.fullmatch(input_text) and validator(input_text):
        try:
            return converter(input_text)
        except Exception as e:
            raise ConversionError(f"Conversion failed: {e}") from e
    else:
        raise ValidationError('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=ValidationError,
                        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 APIError: 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 APIError("options list cannot be empty.")

    if by_number:
        return get_valid_input(prompt=prompt,
                               pattern=INT,
                               validator=is_in_range(1, len(options)),
                               converter=lambda t: int(t)-1,
                               if_incorrect=if_incorrect)
    else:
        return get_valid_input(prompt=prompt,
                               validator=is_in_list(options, case_sensitive),
                               if_incorrect=if_incorrect)


def print_iterable(iterable: Iterable[Any], item_pattern: str = '{}',
                   join_by: str = '\n', start: str = '', end: str = '') -> None:
    """
    Conveniently prints all elements of a one-dimensional iterable, formatted
    using the logic of format_iterable().

    :param iterable: The iterable whose elements are to be formatted and printed.
    :param item_pattern: The format string used for each individual item (e.g., 'Item: {}').
    :param join_by: The separator placed between the formatted elements.
    :param start: A prefix string placed at the beginning of the output.
    :param end: A suffix string placed at the end of the output.
    :return: None. The result is printed directly to stdout.
    """
    print(format_iterable(iterable, item_pattern, join_by, start, end))


def print_zipped_iterable(iterable: Iterable[Iterable[Any]], item_pattern: str = '{}: {}',
                          join_by: str='\n', start: str = '', end: str = '') -> None:
    """
    Conveniently prints elements of an iterable containing unpackable items (e.g., pairs, tuples),
    formatted using the logic of format_zipped_iterable().

    This is useful for items produced by zip(), dict.items(), etc.

    :param iterable: The iterable containing elements that can be unpacked (e.g., [(1, 'a'), (2, 'b')]).
                     Each inner element is unpacked using Python's *item syntax.
    :param item_pattern: The format string used for each inner unpackable element.
                         The number of elements in each inner item MUST match the number of placeholders
                         in 'item_pattern' (e.g., '{}: {}' requires two elements).
    :param join_by: The separator placed between the formatted elements.
    :param start: A prefix string placed at the beginning of the output.
    :param end: A suffix string placed at the end of the output.
    :return: None. The result is printed directly to stdout.
    """
    print(format_zipped_iterable(iterable, item_pattern, join_by, start, end))


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