"""Module to manage fixtures defined in Python packages."""

import inspect
import logging

from asteex.core.decorator.argument import CopyError
from asteex.core.fixture.producer import ProducerFixtureBuilder


class DecorationError(Exception):
    """Exception for errors raised on attempt to decorate callables for producer fixtures."""


class FixtureStorage:
    """Class to store fixtures."""

    def __init__(self):
        self.__fixtures = {}

    def add_fixtures(self, group, fixtures):
        """Add fixtures to the storage.

        :param group: string group name for the fixtures.
        :param fixtures: dictionary with string names of fixtures as keys and iterables with
            :class:`Fixtures <asteex.core.fixture.abstract.Fixture>` as values.
        :raises: :class:`ValueError` if fixtures object is invalid.
        """
        group = str(group)

        logger = logging.getLogger(__name__)
        logger.debug("Try to add fixtures to the group '%s' into the storage %s.", group, self)

        try:
            fixtures_dict = dict(fixtures)
        except (TypeError, ValueError) as error:
            logger.exception("Fixtures map is not a valid dictionary.")
            raise ValueError("Fixtures map is not a valid dictionary") from error

        refined_fixtures = {}
        for name, fixtures_list in fixtures_dict.items():
            name = str(name)

            if name in refined_fixtures:
                logger.error("Fixtures map has conflicting names.")
                raise ValueError("Fixtures map has conflicting names")

            try:
                refined_fixtures_list = tuple(fixtures_list)
            except TypeError as error:
                logger.exception("Can't iterate over fixtures for one of the names.")
                raise ValueError("Can't iterate over fixtures for one of the names") from error

            refined_fixtures[name] = refined_fixtures_list

        fixtures_group = self.__fixtures.setdefault(group, {})

        for name, fixtures_list in refined_fixtures.items():
            fixtures_group.setdefault(name, []).extend(fixtures_list)

        logger.info("Added fixtures to the group '%s' into the storage %s.", group, self)

    def get_fixtures(self, group):
        """Get stored fixtures.

        :param group: string group name with fixtures.
        :returns: dictionary with string names of the fixtures as keys and tuples with
            :class:`Fixtures <asteex.core.fixture.abstract.Fixture>` as values.
        """
        group = str(group)
        fixtures_group = self.__fixtures.get(group, {})
        fixtures = {name: tuple(fixtures) for name, fixtures in fixtures_group.items()}

        logging.getLogger(__name__).info(
            "Got fixtures of group '%s' from the storage %s ",
            group,
            self,
        )

        return fixtures


class ProducerFixtureDecorator(ProducerFixtureBuilder):
    """Decorator to create producer fixtures from callables."""

    def __init__(self):
        super().__init__()
        self.__names = frozenset()
        self.__storages = ()

    def add_storages(self, storages, overwrite=False):
        """Add storages for new fixtures.

        :param storages: iterable with :class:`FixtureStorage`.
        :param overwrite: boolean whether the new storages should overwrite the existing ones.
        :returns: self.
        :raises: :class:`ValueError` if storages is not a valid iterable.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to set fixture storages in %s.", self)

        try:
            refined_fixture_storages = tuple(storages)
        except TypeError as error:
            logger.exception("Can't iterate over fixture storages.")
            raise ValueError("Can't iterate over fixture storages") from error

        if overwrite:
            self.__storages = refined_fixture_storages
        else:
            self.__storages = self.__storages + refined_fixture_storages

        logger.info("Set fixture storages in %s.", self)

        return self

    def get_storages(self):
        """Get storages for new fixtures.

        :returns: tuple with :class:`FixtureStorage`.
        """
        return self.__storages

    def add_names(self, names, overwrite=False):
        """Add names for the fixture.

        :param names: iterable with string names of the fixture.
        :param overwrite: boolean whether the new names should overwrite the existing ones.
        :returns: self.
        :raises: :class:`ValueError` if names is not a valid iterable.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to add fixture names in %s.", self)

        try:
            refined_fixture_names = frozenset(str(name) for name in names)
        except TypeError as error:
            logger.exception("Can't iterate over fixture names.")
            raise ValueError("Can't iterate over fixture names") from error

        if overwrite:
            self.__names = refined_fixture_names
        else:
            self.__names = self.__names.union(refined_fixture_names)

        logger.info("Added fixture names in %s.", self)

        return self

    def get_names(self):
        """Get names of the fixture.

        :returns: set with string names of the fixture.
        """
        return self.__names

    def __call__(self, target):
        """Decorate a callable.

        :param target: a callable to decorate.
        :returns: unchanged target.
        :raises: :class:`DecorationError` if an error occurs on attempt to create or store
            a fixture.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to decorate %s to build a fixture with %s.", target, self)

        module = inspect.getmodule(target)
        if module is None:
            logger.error("Can't get the module of the decorated object for a fixture.")
            raise DecorationError("Can't get the module of the decorated object for a fixture")

        try:
            fixture = self.build_from_callable(target)
        except Exception as error:
            logger.exception("Can't build a fixture.")
            raise DecorationError("Can't build a fixture") from error

        fixture_names = self.get_names()
        if fixture_names:
            refined_fixture_names = [str(name) for name in fixture_names]
        else:
            try:
                refined_fixture_names = [target.__name__]
            except Exception as error:
                logger.exception("Can't get a name for a fixture from the decorated object.")
                raise DecorationError(
                    "Can't get a name for a fixture from the decorated object",
                ) from error

        fixtures = {name: [fixture] for name in refined_fixture_names}

        for storage in self.__storages:
            try:
                storage.add_fixtures(group=module.__name__, fixtures=fixtures)
            except Exception as error:
                logger.exception("Can't store the built fixture.")
                raise DecorationError("Can't store the built fixture") from error

        logger.info("Decorated %s to build a fixture with %s.", target, self)

        return target

    def _copy_data(self, other):
        """Copy data from this instance to another.

        :param other: instance of the same type.
        :raises: :class:`CopyError <asteex.core.decorator.argument.CopyError>`
            if data can't be copied.
        """
        super()._copy_data(other)

        logger = logging.getLogger(__name__)

        try:
            other.add_names(self.get_names())
        except Exception as error:
            logger.exception("Can't copy fixture names to another instance.")
            raise CopyError("Can't copy fixture names to another instance") from error

        try:
            other.add_storages(self.get_storages(), overwrite=True)
        except Exception as error:
            logger.exception("Can't copy fixture storages to another instance.")
            raise CopyError("Can't copy fixture storages to another instance") from error
