"""Module with helpers for test execution via command-line interface."""

import importlib
import pkgutil
import logging.config
import json
import argparse

from asteex.core.reporter.log import LogReporter
from asteex.core.reporter.composite import CompositeReporter
from asteex.runpackage.reporter import Loader as ReporterLoader


class RunPackageError(Exception):
    """Exception raised for errors during invocation of runpackage via command-line."""


class BuildReporterError(Exception):
    """Exception raised on failed attempts to create a reporter."""


def configure_logging(config_path):
    """Configure logging.

    :param config_path: path to the configuration file or None to skip configuration.
    :raises: :class:`RunPackageError` if configuration file does not exist or invalid.
    """
    if config_path is None:
        return

    config_path = str(config_path)

    try:
        with open(config_path, mode="r", encoding="utf8") as config_file:
            config = json.load(config_file)
    except Exception as error:
        raise RunPackageError("Can't read the config file for logging") from error

    try:
        logging.config.dictConfig(config=config)
    except Exception as error:
        raise RunPackageError("Can't apply the config for logging") from error


def load_modules(package):
    """Import all subpackages and submodules of the package.

    :param package: Python package to import.
    :raises: :class:`RunPackageError` if an error occurs during import.
    """
    package = str(package)

    import_stack = [package]
    while import_stack:
        module_name = import_stack.pop()

        try:
            module = importlib.import_module(module_name)
        except Exception as error:
            raise RunPackageError(f"Can't import module '{module_name}'") from error

        module_path = getattr(module, "__path__", None)

        if module_path is not None:
            for module_info in tuple(pkgutil.iter_modules(module_path)):
                submodule_name = ".".join((module_name, module_info.name))
                import_stack.append(submodule_name)


class ReporterBuilder:
    """Class to prepare a single reporter from multiple sources."""

    def __init__(self, loader=ReporterLoader(), reporters=()):
        self.__loader = loader
        self.__reporters = list(reporters)

    @property
    def reporters(self):
        """Nested reporters."""
        return tuple(self.__reporters)

    def add_from_entry_points(self, entry_point_names):
        """Add reporters from entry points.

        :param entry_point_names: iterable with string names of entry points for callables to
            create nested reporters.
        :raises: :class:`BuildReporterError` if entry points are invalid or an error occurs
            on attempt to create nested reporters.
        """
        reporters = []

        try:
            refined_entry_point_names = tuple(str(name) for name in entry_point_names)
        except TypeError as error:
            raise BuildReporterError("Can't iterate over names of entry points") from error

        loader = self.__loader

        try:
            reporters = [
                loader.load_from_entry_point(entry_point_name=name)
                for name in refined_entry_point_names
            ]
        except Exception as error:
            raise BuildReporterError(
                "Failed to create a subreporter from an entry point",
            ) from error

        self.__reporters.extend(reporters)

    def add_from_import_paths(self, import_paths):
        """Add reporters from import paths.

        :param import_paths: iterable with strings in the format "<module>:<object>"
            to import callables to create nested reporters.
        :raises: :class:`BuildReporterError` if import paths are invalid or an error occurs
            on attempt to create nested reporters.
        """
        reporters = []

        try:
            refined_import_paths = tuple(str(path) for path in import_paths)
        except TypeError as error:
            raise BuildReporterError("Can't iterate over import paths") from error

        loader = self.__loader

        try:
            reporters = [
                loader.load_from_import_path(import_path=path) for path in refined_import_paths
            ]
        except Exception as error:
            raise BuildReporterError(
                "Failed to create a subreporter from an import path",
            ) from error

        self.__reporters.extend(reporters)

    def add_from_config_path(self, config_path):
        """Add reporters from a single configuration file.

        :param config_path: string path of the configuration file.
        :raises: :class:`BuildReporterError` if config path is invalid or an error occurs
            on attempt to add reporters from it.
        """
        try:
            reporters = self.build_from_config_path(config_path=config_path)
        except BuildReporterError:
            raise
        except Exception as error:
            raise BuildReporterError("Failed to create subreporters from a config file") from error

        try:
            self.__reporters.extend(reporters)
        except Exception as error:
            raise BuildReporterError("Failed to add subreporters from a config file") from error

    def add_from_config_paths(self, config_paths):
        """Add reporters from multiple configuration files.

        :param config_paths: iterable with string paths of the configuration files.
        :raises: :class:`BuildReporterError` if config paths are invalid or an error occurs
            on attempt to add reporters from one of them.
        """
        try:
            refined_config_paths = tuple(str(path) for path in config_paths)
        except TypeError as error:
            raise BuildReporterError("Can't iterate over config paths") from error

        reporters = []

        for path in refined_config_paths:
            try:
                config_reporters = self.build_from_config_path(config_path=path)
            except BuildReporterError:
                raise
            except Exception as error:
                raise BuildReporterError(
                    "Failed to create subreporters from a config file",
                ) from error

            try:
                reporters.extend(config_reporters)
            except Exception as error:
                raise BuildReporterError("Failed to add subreporters from a config file") from error

        self.__reporters.extend(reporters)

    def build_from_config_path(self, config_path):
        """Create reporters from a single configuration file.

        :param config_path: string path of the configuration file.
        :raises: :class:`BuildReporterError` if config path is invalid or an error occurs
            on attempt to add reporters from it.
        :returns: list with instances of :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        """
        try:
            with open(str(config_path), mode="r", encoding="utf8") as config_file:
                config = json.load(config_file)
        except Exception as error:
            raise BuildReporterError("Can't read the config file for subreporters") from error

        try:
            individual_configs = tuple(config.get("reporters", {}).values())
        except Exception as error:
            raise BuildReporterError("Can't iterate over configs for subreporters") from error

        loader = self.__loader
        try:
            reporters = [
                loader.load_from_config(config=reporter_config)
                for reporter_config in individual_configs
            ]
        except Exception as error:
            raise BuildReporterError("Failed to create a subreporter from a config file") from error

        return reporters

    def build_unified_reporter(self):
        """Join nested reporters into a single one.

        :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
        """
        reporters = self.__reporters

        if not reporters:
            reporter = LogReporter()
        elif len(reporters) == 1:
            reporter = reporters[0]
        else:
            reporter = CompositeReporter(children=reporters)

        return reporter


def prepare_reporter(
    entry_point_names,
    import_paths,
    config_paths,
    builder_factory=ReporterBuilder,
):
    """Prepare a single reporter from multiple subreporters.

    :param entry_point_names: iterable with string names of entry points for callables to
        create subreporters.
    :param import_paths: iterable with string in the format "<module>:<object>" to import callables
        to create subreporters.
    :param config_paths: iterable with string paths of the configuration files for subreporters.
    :param builder_factory: a callable to create a new :class:`ReporterBuilder` to combine
        multiple reporters into one.
    :returns: :class:`Reporter <asteex.abstract.reporter.Reporter>`.
    :raises: :class:`RunPackageError` if an error occurs on attempt to create subreporters from
        entry points, import paths, or config file.
    """
    try:
        builder = builder_factory()
    except Exception as error:
        raise RunPackageError("Can't create a builder to prepare reporters") from error

    try:
        builder.add_from_entry_points(entry_point_names=entry_point_names)
    except Exception as error:
        raise RunPackageError("Can't prepare reporters from entry points") from error

    try:
        builder.add_from_import_paths(import_paths=import_paths)
    except Exception as error:
        raise RunPackageError("Can't prepare reporters from import paths") from error

    try:
        builder.add_from_config_paths(config_paths=config_paths)
    except Exception as error:
        raise RunPackageError("Can't prepare reporters from config paths") from error

    try:
        return builder.build_unified_reporter()
    except Exception as error:
        raise RunPackageError("Can't combine reporters") from error


def create_parser():
    """Create argument parser.

    :returns: instance of :class:`argparse.ArgumentParser`.
    """
    parser = argparse.ArgumentParser(
        prog="asteex.runpackage",
        description="Retest runpackage mode",
    )
    parser.add_argument("package", help="Name of the package with test cases")
    parser.add_argument(
        "--logging-config",
        dest="logging_config_path",
        help="Path to a JSON file to configure logging",
    )
    parser.add_argument(
        "--runner-config",
        dest="runner_config_path",
        help="Path to a JSON file to configure the runner",
    )
    parser.add_argument(
        "--reporter-from-entry-point",
        dest="reporters_entry_point_names",
        action="append",
        default=[],
        help="Add a reporter with a callable referred by an entry point",
    )
    parser.add_argument(
        "--reporter-from-import-path",
        dest="reporters_import_paths",
        action="append",
        default=[],
        help="Add a reporter with a callable referred by an import path",
    )
    parser.add_argument(
        "--reporters-from-config",
        dest="reporters_config_paths",
        action="append",
        default=[],
        help="Add reporters with a JSON config file",
    )

    return parser
