__all__ = ["App"]

import difflib
from typing import Never, TypeAlias

from argenta.app.autocompleter import AutoCompleter
from argenta.app.behavior_handlers.models import (BehaviorHandlersFabric,
                                                  BehaviorHandlersSettersMixin)
from argenta.app.dividing_line.models import DynamicDividingLine, StaticDividingLine
from argenta.app.presentation.renderers import PlainRenderer, Renderer, RichRenderer
from argenta.app.presentation.viewers import Viewer
from argenta.app.protocols import Printer
from argenta.app.registered_routers.entity import RegisteredRouters
from argenta.command.exceptions import (InputCommandException,
                                        RepeatedInputFlagsException,
                                        UnprocessedInputFlagException)
from argenta.command.models import Command, InputCommand
from argenta.response import Response
from argenta.router import Router
from argenta.router.exceptions import (RepeatedAliasNameException,
                                       RepeatedTriggerNameException)

Matches: TypeAlias = list[str] | list[Never]


class BaseApp(BehaviorHandlersSettersMixin):
    def __init__(
        self,
        *,
        prompt: str,
        initial_message: str,
        farewell_message: str,
        exit_command: Command,
        system_router_title: str,
        dividing_line: StaticDividingLine | DynamicDividingLine | None,
        repeat_command_groups_printing: bool,
        override_system_messages: bool,
        autocompleter: AutoCompleter,
        printer: Printer,
    ) -> None:
        self._prompt: str = prompt
        self._printer: Printer = printer
        self._exit_command: Command = exit_command
        self._dividing_line: StaticDividingLine | DynamicDividingLine | None = dividing_line
        self._repeat_command_groups_printing: bool = repeat_command_groups_printing
        self._override_system_messages: bool = override_system_messages
        self._autocompleter: AutoCompleter = autocompleter
        self._system_router: Router = Router(title=system_router_title)

        self.registered_routers: RegisteredRouters = RegisteredRouters()
        self._messages_on_startup: list[str] = []

        self._renderer: Renderer = PlainRenderer() if self._override_system_messages else RichRenderer()

        self._viewer: Viewer = Viewer(
            printer=self._printer,
            renderer=self._renderer,
            dividing_line=self._dividing_line,
            override_system_messages=self._override_system_messages,
        )
        self._handlers_fabric: BehaviorHandlersFabric = BehaviorHandlersFabric(
            printer=self._printer,
            renderer=self._renderer,
            most_similar_command_getter=self._most_similar_command
        )

        self._initial_message: str = self._renderer.render_initial_message(initial_message)
        self._farewell_message: str = self._renderer.render_farewell_message(farewell_message)

        super().__init__(
            description_message_generator = self._handlers_fabric.generate_description_message_generator(),
            incorrect_input_syntax_handler = self._handlers_fabric.generate_incorrect_input_syntax_handler(),
            repeated_input_flags_handler = self._handlers_fabric.generate_repeated_input_flags_handler(),
            empty_input_command_handler = self._handlers_fabric.generate_empty_input_command_handler(),
            unknown_command_handler = self._handlers_fabric.generate_unknown_command_handler(),
            exit_command_handler = self._handlers_fabric.generate_exit_command_handler(self._farewell_message)
        )

    def _is_exit_command(self, command: InputCommand) -> bool:
        if not self._system_router.command_handlers.get_command_handler_by_trigger(command.trigger.lower()):
            return False
        return True

    def _is_unknown_command(self, input_command: InputCommand) -> bool:
        if not self.registered_routers.get_router_by_trigger(input_command.trigger.lower()):
            return True
        return False

    def _error_handler(self, error: InputCommandException, raw_command: str) -> None:
        if isinstance(error, UnprocessedInputFlagException):
            self._incorrect_input_syntax_handler(raw_command)
        elif isinstance(error, RepeatedInputFlagsException):
            self._repeated_input_flags_handler(raw_command)
        else:
            self._empty_input_command_handler()

    def _validate_routers_for_collisions(self) -> None:
        seen_names: set[str] = set()

        for router_entity in self.registered_routers:
            if not seen_names.isdisjoint(router_entity.triggers):
                raise RepeatedTriggerNameException()

            alias_collisions = seen_names.intersection(router_entity.aliases)
            if alias_collisions:
                raise RepeatedAliasNameException(alias_collisions)

            seen_names.update(router_entity.triggers)
            seen_names.update(router_entity.aliases)

    def _most_similar_command(self, unknown_command: str) -> str | None:
        all_commands = self.registered_routers.get_triggers()
        matches = difflib.get_close_matches(unknown_command, all_commands, n=1)
        return matches[0] if matches else None

    def _setup_system_router(self) -> None:
        @self._system_router.command(self._exit_command)
        def _(response: Response) -> None:
            self._exit_command_handler(response)

        self.registered_routers.add_registered_router(self._system_router)

    def _pre_cycle_setup(self) -> None:
        self._setup_system_router()
        self._validate_routers_for_collisions()
        self._autocompleter.initial_setup(self.registered_routers.get_triggers())

        if self._messages_on_startup:
            self._viewer.view_messages_on_startup(self._messages_on_startup)

        if not self._repeat_command_groups_printing:
            self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)

    def _process_exist_and_valid_command(self, input_command: InputCommand) -> None:
        processing_router = self.registered_routers.get_router_by_trigger(input_command.trigger.lower())

        if not processing_router:
            raise RuntimeError(f"Router for '{input_command.trigger}' not found. Panic!")

        self._viewer.view_framed_text_from_generator(
            output_text_generator=lambda: processing_router.finds_appropriate_handler(input_command),
            is_stdout_redirected_by_router=processing_router.is_redirect_stdout_disabled
        )

    def _run_repl(self) -> None:
        self._viewer.view_initial_message(self._initial_message)
        self._pre_cycle_setup()
        while True:
            if self._repeat_command_groups_printing:
                self._viewer.view_command_groups_description(self._description_message_generator, self.registered_routers)

            print()  # pre-prompt gap
            raw_command: str = self._autocompleter.prompt(self._renderer.render_prompt(self._prompt))
            print()  # post-prompt gap

            try:
                input_command: InputCommand = InputCommand.parse(raw_command=raw_command)
            except InputCommandException as error:  # noqa F841
                self._viewer.view_framed_text_from_generator(
                    output_text_generator=lambda: self._error_handler(error, raw_command) # noqa
                )
                continue

            if self._is_unknown_command(input_command):
                self._viewer.view_framed_text_from_generator(
                    output_text_generator=lambda: self._unknown_command_handler(input_command)
                )
                continue

            if self._is_exit_command(input_command):
                self._system_router.finds_appropriate_handler(input_command)
                return

            self._process_exist_and_valid_command(input_command)


class App(BaseApp):
    def __init__(
        self,
        *,
        prompt: str = ">>> ",
        initial_message: str = "Argenta",
        farewell_message: str = "See you",
        exit_command: Command = Command("q", description="Exit command"),
        system_router_title: str = "System points:",
        dividing_line: StaticDividingLine | DynamicDividingLine | None = None,
        repeat_command_groups_printing: bool = False,
        override_system_messages: bool = False,
        autocompleter: AutoCompleter | None = None,
        printer: Printer | None = None,
    ) -> None:
        """
        Public. The essence of the application itself.
        Configures and manages all aspects of the behavior and presentation of the user interacting with the user
        :param prompt: displayed before entering the command
        :param initial_message: displayed at the start of the app
        :param farewell_message: displayed at the end of the app
        :param exit_command: the entity of the command that will be terminated when entered
        :param system_router_title: system router title
        :param dividing_line: the entity of the dividing line
        :param repeat_command_groups_printing: whether to repeat the available commands and their description
        :param override_system_messages: whether to redefine the default formatting of system messages
        :param autocompleter: the entity of the autocompleter
        :param printer: system messages text output function
        :return: None
        """
        from rich.console import Console
        super().__init__(
            prompt=prompt,
            initial_message=initial_message,
            farewell_message=farewell_message,
            exit_command=exit_command,
            system_router_title=system_router_title,
            dividing_line=dividing_line,
            repeat_command_groups_printing=repeat_command_groups_printing,
            override_system_messages=override_system_messages,
            autocompleter=autocompleter or AutoCompleter(),
            printer=printer or Console().print,
        )

    def include_router(self, router: Router) -> None:
        """
        Public. Registers the router in the application
        :param router: registered router
        :return: None
        """
        self.registered_routers.add_registered_router(router)

    def include_routers(self, *routers: Router) -> None:
        """
        Public. Registers the routers in the application
        :param routers: registered routers
        :return: None
        """
        for router in routers:
            self.include_router(router)

    def add_message_on_startup(self, message: str) -> None:
        """
        Public. Adds a message that will be displayed when the application is launched
        :param message: the message being added
        :return: None
        """
        self._messages_on_startup.append(message)
