"""
Contains an implementation of an Interactive Searcher for an
interactive cli experience.
"""

from __future__ import annotations

try:
    import questionary
    from questionary.prompts.common import Choice
except ImportError as e:
    raise ImportError(
        "Grascii: interactive extra dependencies are not installed"
    ) from e

import sys
from typing import TYPE_CHECKING, TypeVar

from grascii import metrics, regen
from grascii.dictionary import Dictionary
from grascii.dictionary.list import get_built_ins, get_installed
from grascii.interpreter import Interpretation, interpretation_to_string
from grascii.parser import InvalidGrascii
from grascii.searchers import (
    GrasciiSearcher,
    GrasciiSearchOptions,
    SearcherOptions,
    SearchResult,
)

if TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Sequence

    if sys.version_info >= (3, 11):
        from typing import Unpack
    else:
        from typing_extensions import Unpack

T = TypeVar("T")


class InteractiveSearcher(GrasciiSearcher):
    """This subclass of GrasciiSearcher runs an interactive search
    experience for performing grascii searches with support for changing
    search parameters.
    """

    def __init__(self, **kwargs: Unpack[SearcherOptions]):
        super().__init__(**kwargs)
        self.available_dicts = set(self.dictionaries)
        installed = map(lambda s: Dictionary.new(s), get_installed())
        built_ins = map(lambda s: Dictionary.new(s), get_built_ins())
        self.available_dicts.update(installed, built_ins)

    def search(
        self, *, grascii: str | None = None, **kwargs: Unpack[GrasciiSearchOptions]
    ) -> None:
        """
        :param grascii: Ignored
        """
        self._extract_search_args(**kwargs)
        self._metric = None
        self._run_interactive()

    def sorted_search(
        self,
        metric: Callable[
            [SearchResult[Interpretation]], metrics.Comparable
        ] = metrics.grascii_standard,
        *,
        grascii: str | None = None,
        **kwargs: Unpack[GrasciiSearchOptions],
    ) -> None:
        """
        :param grascii: Ignored
        """
        self._extract_search_args(**kwargs)
        self._metric = metric
        self._run_interactive()

    def _choose_interpretation(
        self, interpretations: Sequence[Interpretation]
    ) -> int | None:
        """Prompt the user to choose an interpretation(s).

        :param interpretations: A collection of Interpretations to present
            to the user
        :returns: The index of the selected item in interpretations.
        """

        if len(interpretations) == 1:
            return 0
        prompt = "Choose an interpretation to use in the search:"
        choices = [Choice(title="all", value=0)]
        for i, interp in enumerate(interpretations):
            choices.append(Choice(title=interpretation_to_string(interp), value=i + 1))
        return questionary.select(prompt, choices).ask()

    def _run_interactive(self) -> None:
        """Run an interactive search loop."""

        previous_search = None
        while True:
            action = questionary.select(
                "What would you like to do?",
                [
                    "New Search",
                    Choice("Modify Search", disabled=not previous_search),
                    "Edit Settings",
                    "Exit",
                ],
            ).ask()

            if action is None or action == "Exit":
                exit()
            elif action == "New Search":
                previous_search = self._interactive_search()
            elif action == "Modify Search":
                previous_search = self._interactive_search(previous_search)
            elif action == "Edit Settings":
                while True:
                    action = questionary.select(
                        "Search Settings",
                        [
                            Choice(
                                title=f"Uncertainty [{self.uncertainty}]",
                                value=1,
                            ),
                            Choice(
                                title=f"Search Mode [{self.search_mode.value}]",
                                value=2,
                            ),
                            Choice(
                                title=f"Annotation Mode [{self.annotation_mode.value}]",
                                value=3,
                            ),
                            Choice(
                                title=f"Aspirate Mode [{self.aspirate_mode.value}]",
                                value=4,
                            ),
                            Choice(
                                title=f"Disjoiner Mode [{self.disjoiner_mode.value}]",
                                value=5,
                            ),
                            Choice(title=f"Fix First [{self.fix_first}]", value=6),
                            Choice(
                                title=f"Dictionaries [{len(self.dictionaries)} selected]",
                                value=7,
                            ),
                            "Back",
                        ],
                    ).ask()

                    if action is None or action == "Back":
                        break
                    elif action == 1:
                        self._change_arg(
                            "uncertainty", range(3), display_name="Uncertainty"
                        )
                    elif action == 2:
                        self._change_arg(
                            "search_mode",
                            list(regen.SearchMode),
                            convert=self._get_enum_value,
                            display_name="Search mode",
                        )
                    elif action == 3:
                        self._change_arg(
                            "annotation_mode",
                            list(regen.Strictness),
                            convert=self._get_enum_value,
                            display_name="Annotation mode",
                        )
                    elif action == 4:
                        self._change_arg(
                            "aspirate_mode",
                            list(regen.Strictness),
                            convert=self._get_enum_value,
                            display_name="Aspirate mode",
                        )
                    elif action == 5:
                        self._change_arg(
                            "disjoiner_mode",
                            list(regen.Strictness),
                            convert=self._get_enum_value,
                            display_name="Disjoiner mode",
                        )
                    elif action == 6:
                        self._change_arg(
                            "fix_first", [True, False], display_name="Fix First"
                        )
                    elif action == 7:
                        self._select_dictionaries()

    def _get_enum_value(self, enum):
        return enum.value

    def _change_arg(
        self,
        arg_name,
        options: Iterable[T],
        display_name: str | None = None,
        convert: Callable[[T], str] = str,
    ) -> None:
        """Prompt the user to select the value of a search parameter and set
            the new value.

        :param arg_name: The name of the argument to change.
        :param options: A collection of choices to present to the user.
        :param display_name: The title of the prompt.
        :param convert: A function that returns a string given an object of
            type T.
        """

        choices = []
        for option in options:
            selected = getattr(self, arg_name) == option
            choices.append(
                Choice(title=convert(option), value=option, checked=selected)
            )
        title = display_name if display_name else "Set " + arg_name
        setting = questionary.select(title, choices).ask()
        if setting is not None:
            setattr(self, arg_name, setting)

    def _select_dictionaries(self) -> None:
        choices = []
        for dictionary in self.available_dicts:
            selected = dictionary in self.dictionaries
            choices.append(
                Choice(title=dictionary.name, value=dictionary, checked=selected)
            )
        title = "Choose Dictionaries"
        dictionaries = questionary.checkbox(
            title,
            choices,
            validate=lambda d: True if len(d) > 0 else "Select at least one dictionary",
        ).ask()
        if dictionaries:
            self.dictionaries = dictionaries

    def _interactive_search(self, previous: str | None = None) -> str | None:
        """Run an interactive search.

        :param previous: The previous search performed in this interactive
            session.
        :returns: The search string used.
        """

        search, interpretations = self._get_grascii_search(previous)
        if search is None:
            return previous
        assert interpretations is not None
        index = self._choose_interpretation(interpretations)
        if index is None:
            return search
        builder = regen.RegexBuilder(
            uncertainty=self.uncertainty,
            search_mode=self.search_mode,
            fix_first=self.fix_first,
            annotation_mode=self.annotation_mode,
            aspirate_mode=self.aspirate_mode,
            disjoiner_mode=self.disjoiner_mode,
        )
        interps = interpretations if index == 0 else interpretations[index - 1 : index]
        patterns = builder.generate_patterns_map(interps)
        starting_letters = builder.get_starting_letters(interps)
        results = self.perform_search(patterns, starting_letters)
        if self._metric:
            results = sorted(results, key=self._metric)
        count = 0
        display_all = False
        for result in results:
            action = "Next"
            if not display_all:
                action = questionary.select(
                    "Search Results", ["Next", "Display All", "End Search"]
                ).ask()
            if action is None or action == "End Search":
                break
            elif action == "Display All":
                display_all = True
            print(result.entry.grascii, result.entry.translation)
            count += 1

        print("Results:", count)
        print()
        return search

    def _get_grascii_search(
        self, previous: str | None = None
    ) -> tuple[str, list[Interpretation]] | tuple[None, None]:
        """Prompt the user for a grascii string.

        :param previous: The previous grascii string used in a search.
        :returns: A grascii string and associated interpretations
        """

        while True:
            if previous:
                search = questionary.text("Enter Search:", default=previous).ask()
            else:
                search = questionary.text("Enter Search:").ask()

            if search is None:
                return search, None
            if search == "":
                continue
            search = search.upper()
            try:
                interpretations = list(self._parser.interpret(search))
            except InvalidGrascii as e:
                print("Invalid Grascii String", file=sys.stderr)
                print(e.context, file=sys.stderr)
                previous = search
                continue
            return search, interpretations
