"""Module to manage reporters for tests from Python packages."""

import importlib
import importlib.metadata
import re


ENTRY_POINT_GROUP = "asteex.reporter_factory"


class LoadError(Exception):
    """Exception raised during attempts to load a reporter."""


class Loader:
    """Class to load reporters."""

    def load_from_entry_point(
        self,
        entry_point_name,
        positional_arguments=(),
        keyword_arguments=(),
    ):
        """Load a reporter from an entry point.

        :param entry_point_name: string name of the entry point for a callable to build
            a new reporter.
        :param positional_arguments: iterable with positional arguments for the callable.
        :param keyword_arguments: map with keyword arguments for the callable.
        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        :raises: :class:`LoadError` if the factory can't be loaded or an error occurs on
            attempt to build a reporter.
        """
        entry_point_name = str(entry_point_name)
        entry_points = importlib.metadata.entry_points(group=ENTRY_POINT_GROUP)

        try:
            factory = entry_points[entry_point_name].load()
        except Exception as error:
            raise LoadError("Can't load the entry point for a reporter") from error

        try:
            refined_positional_arguments = list(positional_arguments)
        except TypeError as error:
            raise LoadError(
                "Can't iterate over positional arguments for the factory for a reporter",
            ) from error

        try:
            keyword_arguments_dict = dict(keyword_arguments)
        except (TypeError, ValueError) as error:
            raise LoadError(
                "Keyword arguments for the factory for a reporter are not a valid dictionary",
            ) from error

        refined_keyword_arguments = {
            str(argument): value for argument, value in keyword_arguments_dict.items()
        }

        if len(keyword_arguments) > len(refined_keyword_arguments):
            raise LoadError(
                "One of keyword arguments for the factory for a reporter appears multiple times",
            )

        try:
            reporter = factory(*refined_positional_arguments, **refined_keyword_arguments)
        except Exception as error:
            raise LoadError("Can't create a reporter") from error

        return reporter

    def load_from_import_path(self, import_path, positional_arguments=(), keyword_arguments=()):
        """Import a reporter from a Python module.

        :param import_path: string in the format "<module>:<object>" to import a callable to build
            a new reporter.
        :param positional_arguments: iterable with positional arguments for the callable.
        :param keyword_arguments: map with keyword arguments for the callable.
        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        :raises: :class:`LoadError` if the factory can't be imported or an error occurs on
            attempt to build a reporter.
        """
        import_path = str(import_path)

        import_path_pattern = re.compile(r"(?P<module>[\w.]+):(?P<attributes>[\w.]+)")

        matched_import_path = import_path_pattern.fullmatch(import_path)
        if matched_import_path is None:
            raise LoadError("Import path for a reporter is invalid")

        try:
            factory = importlib.import_module(matched_import_path.group("module"))
        except Exception as error:
            raise LoadError("Can't import the module of a reporter") from error

        for attribute_name in matched_import_path.group("attributes").split("."):
            try:
                factory = getattr(factory, attribute_name)
            except Exception as error:
                raise LoadError("Can't get a factory for a reporter") from error

        try:
            refined_positional_arguments = list(positional_arguments)
        except TypeError as error:
            raise LoadError(
                "Can't iterate over positional arguments for the factory for a reporter",
            ) from error

        try:
            keyword_arguments_dict = dict(keyword_arguments)
        except (TypeError, ValueError) as error:
            raise LoadError(
                "Keyword arguments for the factory for a reporter are not a valid dictionary",
            ) from error

        refined_keyword_arguments = {
            str(argument): value for argument, value in keyword_arguments_dict.items()
        }

        if len(keyword_arguments) > len(refined_keyword_arguments):
            raise LoadError(
                "One of keyword arguments for the factory for a reporter appears multiple times",
            )

        try:
            reporter = factory(*refined_positional_arguments, **refined_keyword_arguments)
        except Exception as error:
            raise LoadError("Can't create a reporter") from error

        return reporter

    def load_from_entry_point_config(self, config):
        """Parse configuration to build a reporter from an entry point.

        :param config: dictionary with configuration for a reporter.
        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        :raises: :class:`LoadError` if configuration is invalid.
        """
        try:
            entry_point_name = str(config["entry_point_name"])
        except Exception as error:
            raise LoadError("Can't get a name of an entry point") from error

        try:
            positional_arguments=list(config.get("args", []))
        except Exception as error:
            raise LoadError("Can't get positional arguments") from error

        try:
            keyword_arguments=dict(config.get("kwargs", {}))
        except Exception as error:
            raise LoadError("Can't get keyword arguments") from error

        try:
            reporter = self.load_from_entry_point(
                entry_point_name=entry_point_name,
                positional_arguments=positional_arguments,
                keyword_arguments=keyword_arguments,
            )
        except LoadError:
            raise
        except Exception as error:
            raise LoadError("Unexpected error occured on attempt to create a reporter") from error

        return reporter

    def load_from_import_path_config(self, config):
        """Parse configuration to build a reporter from an import path.

        :param config: dictionary with configuration for a reporter.
        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        :raises: :class:`LoadError` if configuration is invalid.
        """
        try:
            import_path = str(config["import_path"])
        except Exception as error:
            raise LoadError("Can't get an import path") from error

        try:
            positional_arguments=list(config.get("args", []))
        except Exception as error:
            raise LoadError("Can't get positional arguments") from error

        try:
            keyword_arguments=dict(config.get("kwargs", {}))
        except Exception as error:
            raise LoadError("Can't get keyword arguments") from error

        try:
            reporter = self.load_from_import_path(
                import_path=import_path,
                positional_arguments=positional_arguments,
                keyword_arguments=keyword_arguments,
            )
        except LoadError:
            raise
        except Exception as error:
            raise LoadError("Unexpected error occured on attempt to create a reporter") from error

        return reporter

    def load_from_config(self, config):
        """Parse configuration to build a reporter.

        :param config: dictionary with configuration for a reporter.
        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        :raises: :class:`LoadError` if configuration is invalid or the type is not supported.
        """
        try:
            config_type = config["type"]
        except Exception as error:
            raise LoadError("Can't get the config type") from error

        if config_type == "entry point":
            load_method = self.load_from_entry_point_config
        elif config_type == "import path":
            load_method = self.load_from_import_path_config
        else:
            raise LoadError("Config type is not supported")

        try:
            reporter = load_method(config=config)
        except LoadError:
            raise
        except Exception as error:
            raise LoadError(
                "Unexpected error occured on attempt to create a reporter",
            ) from error

        return reporter
