from __future__ import annotations

import contextlib
import dataclasses
import functools
import inspect
import typing

import chitragupt.abc.generic as abc_generic
import chitragupt.exceptions as exceptions

# Type definitions
T = typing.TypeVar("T")
CommandFunction = typing.Callable[..., T]


@dataclasses.dataclass
class ChitraguptPlugin:
    """
    Represents a plugin containing one or more commands.

    Parameters
    ----------
    name : str
        The plugin name.
    description : str
        A brief description of the plugin.
    requirements : List[str], optional
        A list of package requirements (default is empty).
    commands : List[ChitraguptCommand[Any]], optional
        The list of registered commands (default is empty).
    """

    name: str
    description: str
    requirements: typing.List[str] = dataclasses.field(default_factory=list)
    commands: typing.List[abc_generic.ChitraguptCommand] = dataclasses.field(
        default_factory=list
    )

    def __post_init__(self) -> None:
        """Initialize colorama if available for terminal color support."""
        with contextlib.suppress(ImportError):
            import colorama

            colorama.init()

    def __repr__(self) -> str:
        """Return a colored string representation of the plugin if colorama is available."""
        try:
            import colorama

            return (
                f"{colorama.Fore.CYAN}Plugin:{colorama.Fore.RESET} {self.name}\n"
                f"   ↳  {colorama.Fore.RED}{self.description}{colorama.Fore.RESET}"
            )
        except ImportError:
            return f"Plugin: {self.name}\n   ↳  {self.description}"

    def add_requirements(
        self,
        requirements: typing.List[str],
    ) -> None:
        """
        Set the package requirements for the plugin.

        Parameters
        ----------
        requirements : List[str]
            A list of package names required by the plugin.
        """
        self.requirements = requirements

    def register(
        self,
        command_name: str,
        *args: abc_generic.DataType,
        **kwargs: abc_generic.DataType,
    ) -> typing.Callable[[CommandFunction[T]], CommandFunction[T]]:
        """
        Register a new command with the plugin.

        Parameters
        ----------
        command_name : str
            The name of the command.
        *args : DataType
            Positional argument types. (Not allowed - use keyword arguments only.)
        **kwargs : DataType
            Keyword argument types. The key is the parameter name and the value must be a DataType member.

        Returns
        -------
        Callable[[CommandFunction[T]], CommandFunction[T]]
            A decorator that registers the command.

        Raises
        ------
        ChitraGuptCommandRegisteredError
            If a command with the given name is already registered.
        ChitraGuptInvalidDataTypeError
            If an invalid data type is specified or if positional parameters are used.
        """
        # Disallow positional parameters.
        if args:
            raise exceptions.ChitraGuptInvalidDataTypeError(
                "Positional parameters are not allowed. Use keyword arguments with DataType enum values."
            )

        def decorator(func: CommandFunction[T]) -> CommandFunction[T]:
            if any(command.name == command_name for command in self.commands):
                raise exceptions.ChitraGuptCommandRegisteredError(
                    f"Command '{command_name}' is already registered."
                )

            argument_list: typing.List[abc_generic.ChitraGuptParameter] = []

            # Process keyword argument types (only allowed way).
            for key, value in kwargs.items():
                if not isinstance(value, abc_generic.DataType):
                    print(value)
                    print(type(value))
                    raise exceptions.ChitraGuptInvalidDataTypeError(
                        f"Invalid data type for parameter '{key}': {value}"
                    )
                # Determine optionality based on the enum value.
                optional = value in {
                    abc_generic.DataType.OSTRING,
                    abc_generic.DataType.OINTEGER,
                    abc_generic.DataType.OFLOAT,
                    abc_generic.DataType.OBOOL,
                }
                argument_list.append(
                    abc_generic.ChitraGuptParameter(
                        name=key,
                        data_type=value,
                        optional=optional,
                    )
                )

            # Ensure that the function signature includes a "framework" parameter.
            sig = inspect.signature(func)
            params = list(sig.parameters.values())
            if "framework" not in sig.parameters:
                params.insert(
                    0,
                    inspect.Parameter(
                        "framework",
                        inspect.Parameter.POSITIONAL_OR_KEYWORD,
                    ),
                )

            # Update optional parameters: set default to None for matching parameter names.
            for arg in argument_list:
                for i, p in enumerate(params):
                    if p.name == arg.name:
                        params[i] = p.replace(default=None)
                        break

            new_sig = sig.replace(parameters=params)

            @functools.wraps(func)
            def wrapper(
                *func_args: typing.Any, **func_kwargs: typing.Any
            ) -> T:
                # Ensure 'framework' is provided.
                if "framework" not in func_kwargs and func_args:
                    framework_instance = func_args[0]
                    func_kwargs["framework"] = framework_instance
                    func_args = func_args[1:]
                # Validate parameter values.
                for argument in argument_list:
                    if argument.name in func_kwargs:
                        argument.validate_value(func_kwargs[argument.name])
                return func(*func_args, **func_kwargs)

            wrapper.__signature__ = new_sig  # type: ignore

            self.commands.append(
                abc_generic.ChitraguptCommand(
                    name=command_name,
                    parameters=argument_list,
                    function=typing.cast(CommandFunction[T], wrapper),
                )
            )
            return wrapper

        return decorator

    def get_function(
        self,
        command_name: str,
    ) -> CommandFunction[typing.Any]:
        """
        Retrieve the function for a registered command by its name.

        Parameters
        ----------
        command_name : str
            The name of the command to retrieve.

        Returns
        -------
        CommandFunction[Any]
            The function implementing the command.

        Raises
        ------
        KeyError
            If no command with the given name is registered.
        """
        command = next(
            (cmd for cmd in self.commands if cmd.name == command_name), None
        )
        if not command:
            raise KeyError(f"Command '{command_name}' is not registered.")
        return command.function

    def list_commands(self) -> str:
        """
        List all commands registered in the plugin in a formatted string.

        Returns
        -------
        str
            A formatted string listing each command with its parameters and documentation.
        """
        lines = [
            f"Plugin: {self.name}",
            f"Description: {self.description}",
            "Commands:",
        ]
        for cmd in self.commands:
            params = ", ".join(
                f"{p.name} ({p.data_type.name}{'?' if p.optional else ''})"
                for p in cmd.parameters
            )
            doc = (
                cmd.function.__doc__.strip()
                if cmd.function.__doc__
                else "No description provided."
            )
            lines.append(f" - {cmd.name}: {params}\n   {doc}")
        return "\n".join(lines)

    def unregister_command(self, command_name: str) -> None:
        """
        Unregister a command from the plugin by its name.

        Parameters
        ----------
        command_name : str
            The name of the command to remove.

        Raises
        ------
        KeyError
            If the command is not found.
        """
        for i, cmd in enumerate(self.commands):
            if cmd.name == command_name:
                del self.commands[i]
                return
        raise KeyError(
            f"Command '{command_name}' not found in plugin '{self.name}'."
        )

    def update_command_function(
        self,
        command_name: str,
        new_function: CommandFunction[typing.Any],
    ) -> None:
        """
        Update the function implementation of a registered command.

        Parameters
        ----------
        command_name : str
            The name of the command to update.
        new_function : CommandFunction[Any]
            The new function to replace the existing command function.

        Raises
        ------
        KeyError
            If the command is not found.
        """
        for cmd in self.commands:
            if cmd.name == command_name:
                cmd.function = new_function
                return
        raise KeyError(
            f"Command '{command_name}' not found in plugin '{self.name}'."
        )

    def sort_commands(self) -> None:
        """Sort the registered commands alphabetically by their name."""
        self.commands.sort(key=lambda cmd: cmd.name)

    def find_command(
        self,
        command_name: str,
    ) -> typing.Optional[abc_generic.ChitraguptCommand[typing.Any]]:
        """
        Find a command by name (case-insensitive).

        Parameters
        ----------
        command_name : str
            The name of the command to search for.

        Returns
        -------
        Optional[ChitraguptCommand[Any]]
            The matching command, or None if not found.
        """
        return next(
            (
                cmd
                for cmd in self.commands
                if cmd.name.lower() == command_name.lower()
            ),
            None,
        )

    def plugin_summary(self) -> str:
        """
        Generate a summary of the plugin.

        Returns
        -------
        str
            A summary including the plugin name, description, number of commands, and requirements.
        """
        reqs = ", ".join(self.requirements) if self.requirements else "None"
        return (
            f"Plugin: {self.name}\n"
            f"Description: {self.description}\n"
            f"Commands Registered: {len(self.commands)}\n"
            f"Requirements: {reqs}"
        )
