# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""
Input validators for use with Prompt Toolkit and interactive Falyx CLI workflows.

This module defines reusable `Validator` instances and subclasses that enforce valid
user input during prompts—especially for selection actions, confirmations, and
argument parsing.

Included Validators:
- CommandValidator: Validates if the input matches a known command.
- int_range_validator: Enforces numeric input within a range.
- key_validator: Ensures the entered value matches a valid selection key.
- yes_no_validator: Restricts input to 'Y' or 'N'.
- word_validator / words_validator: Accepts specific valid words (case-insensitive).
- MultiIndexValidator: Validates numeric list input (e.g. "1,2,3").
- MultiKeyValidator: Validates string key list input (e.g. "A,B,C").

These validators integrate directly into `PromptSession.prompt_async()` to
enforce correctness and provide helpful error messages.
"""
from typing import TYPE_CHECKING, KeysView, Sequence

from prompt_toolkit.validation import ValidationError, Validator

if TYPE_CHECKING:
    from falyx.falyx import Falyx


class CommandValidator(Validator):
    """Validator to check if the input is a valid command."""

    def __init__(self, falyx: "Falyx", error_message: str) -> None:
        super().__init__()
        self.falyx = falyx
        self.error_message = error_message

    def validate(self, document) -> None:
        if not document.text:
            raise ValidationError(
                message=self.error_message,
                cursor_position=len(document.text),
            )

    async def validate_async(self, document) -> None:
        text = document.text
        if not text:
            raise ValidationError(
                message=self.error_message,
                cursor_position=len(text),
            )
        is_preview, choice, _, __ = await self.falyx.get_command(text, from_validate=True)
        if is_preview:
            return None
        if not choice:
            raise ValidationError(
                message=self.error_message,
                cursor_position=len(text),
            )


def int_range_validator(minimum: int, maximum: int) -> Validator:
    """Validator for integer ranges."""

    def validate(text: str) -> bool:
        try:
            value = int(text)
            if not minimum <= value <= maximum:
                return False
            return True
        except ValueError:
            return False

    return Validator.from_callable(
        validate,
        error_message=f"Invalid input. Enter a number between {minimum} and {maximum}.",
    )


def key_validator(keys: Sequence[str] | KeysView[str]) -> Validator:
    """Validator for key inputs."""

    def validate(text: str) -> bool:
        if text.upper() not in [key.upper() for key in keys]:
            return False
        return True

    return Validator.from_callable(
        validate, error_message=f"Invalid input. Available keys: {', '.join(keys)}."
    )


def yes_no_validator() -> Validator:
    """Validator for yes/no inputs."""

    def validate(text: str) -> bool:
        if text.upper() not in ["Y", "N"]:
            return False
        return True

    return Validator.from_callable(validate, error_message="Enter 'Y', 'y' or 'N', 'n'.")


def words_validator(
    keys: Sequence[str] | KeysView[str], error_message: str | None = None
) -> Validator:
    """Validator for specific word inputs."""

    def validate(text: str) -> bool:
        if text.upper() not in [key.upper() for key in keys]:
            return False
        return True

    if error_message is None:
        error_message = f"Invalid input. Choices: {{{', '.join(keys)}}}."

    return Validator.from_callable(validate, error_message=error_message)


def word_validator(word: str) -> Validator:
    """Validator for specific word inputs."""
    if word.upper() == "N":
        raise ValueError(
            "The word 'N' is reserved for yes/no validation. Use yes_no_validator instead."
        )

    def validate(text: str) -> bool:
        if text.upper().strip() == "N":
            return True
        return text.upper().strip() == word.upper()

    return Validator.from_callable(validate, error_message=f"Enter '{word}' or 'N', 'n'.")


class MultiIndexValidator(Validator):
    def __init__(
        self,
        minimum: int,
        maximum: int,
        number_selections: int | str,
        separator: str,
        allow_duplicates: bool,
        cancel_key: str,
    ) -> None:
        self.minimum = minimum
        self.maximum = maximum
        self.number_selections = number_selections
        self.separator = separator
        self.allow_duplicates = allow_duplicates
        self.cancel_key = cancel_key
        super().__init__()

    def validate(self, document):
        selections = [
            index.strip() for index in document.text.strip().split(self.separator)
        ]
        if not selections or selections == [""]:
            raise ValidationError(message="Select at least 1 item.")
        if self.cancel_key in selections and len(selections) == 1:
            return
        elif self.cancel_key in selections:
            raise ValidationError(message="Cancel key must be selected alone.")
        for selection in selections:
            try:
                index = int(selection)
                if not self.minimum <= index <= self.maximum:
                    raise ValidationError(
                        message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
                    )
            except ValueError:
                raise ValidationError(
                    message=f"Invalid selection: {selection}. Select a number between {self.minimum} and {self.maximum}."
                )
            if not self.allow_duplicates and selections.count(selection) > 1:
                raise ValidationError(message=f"Duplicate selection: {selection}")
        if isinstance(self.number_selections, int):
            if self.number_selections == 1 and len(selections) > 1:
                raise ValidationError(message="Invalid selection. Select only 1 item.")
            if len(selections) != self.number_selections:
                raise ValidationError(
                    message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
                )


class MultiKeyValidator(Validator):
    def __init__(
        self,
        keys: Sequence[str] | KeysView[str],
        number_selections: int | str,
        separator: str,
        allow_duplicates: bool,
        cancel_key: str,
    ) -> None:
        self.keys = keys
        self.separator = separator
        self.number_selections = number_selections
        self.allow_duplicates = allow_duplicates
        self.cancel_key = cancel_key
        super().__init__()

    def validate(self, document):
        selections = [key.strip() for key in document.text.strip().split(self.separator)]
        if not selections or selections == [""]:
            raise ValidationError(message="Select at least 1 item.")
        if self.cancel_key in selections and len(selections) == 1:
            return
        elif self.cancel_key in selections:
            raise ValidationError(message="Cancel key must be selected alone.")
        for selection in selections:
            if selection.upper() not in [key.upper() for key in self.keys]:
                raise ValidationError(message=f"Invalid selection: {selection}")
            if not self.allow_duplicates and selections.count(selection) > 1:
                raise ValidationError(message=f"Duplicate selection: {selection}")
        if isinstance(self.number_selections, int):
            if self.number_selections == 1 and len(selections) > 1:
                raise ValidationError(message="Invalid selection. Select only 1 item.")
            if len(selections) != self.number_selections:
                raise ValidationError(
                    message=f"Select exactly {self.number_selections} items separated by '{self.separator}'"
                )
