from __future__ import annotations

from typing import Any, Iterable, Mapping, Optional, cast, Iterator
from .config_var import ConfigVar
from .utils import get_attribute_docstrings, NameSpace


class ConfigVarCollector:
    """
    Collector class to gather ConfigVar from classes and their nested NameSpace classes.
        This class is used in the AppConfigMeta metaclass to collect ConfigVar defined in the various base
        classes and merge them to create the _config_var_defs
        It acts as a context manager to let the AppConfigMeta metaclass decide which classes are to be scanned
        and in which order. The meta class will call the collect_config_vars on as many classes as needed and will
        get the collected variables at the end of its scanning process.

        collect_config_vars is the main method:
        - it first loop over the attributes declared in the targeted class by scanning its __dict__.
            - when it finds a ConfigVar:
              - it adds it to the collected variables
              - attach the namespace and fq_name to the ConfigVar instance.
              - If the ConfigVar instance doesn't have help defined it tries to use the attribute
              docstring as help.

        - Then it merges the collected variables with the already collected variables from its previous runs.
        - The merge strategy is the following:
          - We need to merge the collected ConfigVar attributes found in the various classes and keep the order of their definition,
            to do so:
            - we loop over the classes _config_var_defs in reversed MRO class order from the furthest ancestor to the closest parent to
              keep python order of class definition (last definition wins).
                - when we discover an new attribute not yet defined we add it at the last position.
                - when discovering an attribute that was already defined in an ancestor we remove the old one and add the new one its new
                position at the end of the list.
            - For Namespaced attribute (defined in a nested NameSpace class) we add them in the same way as other attributes, collisions
              are detected by accounting for the namespace path. Conflicts are resolved in the same way as other attributes. The main difference
              is when a namespace is masked by a non-namespace attribute defined later in the MRO, in this case we need to remove all
              the attributes defined in the masked namespace (including nested namespaces).
    """

    _collected_vars: dict[str, ConfigVar]
    """  The collected ConfigVar indexed by their fq_name. This is the main output of the collector, 
         it will be used to create the _config_var_defs attribute of the AppConfig class."""
    _collected_namespaces: dict[tuple[str, ...], list[str]]
    """ A dict with all the namespaces that have been collected along with the list of the variable fq_name they contain, this is used to detect 
        namespace masking when merging new variables."""
    _auto_prolog_var_names: list[str]
    """ The list of the names of ConfigVar whose attribute auto_prolog is True, this is used to know which variables to put in the prolog file when calling save_prolog."""

    def __enter__(self):
        """Enter the runtime context related to this object."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Exit the runtime context and clean up resources."""
        self.close()

    def __init__(self) -> None:
        """Create a new ConfigVarCollector instance."""
        self._collected_vars = {}
        self._collected_namespaces = {}
        self._auto_prolog_var_names = []

    def close(self) -> None:
        """Clean up resources if needed."""
        self._collected_vars.clear()
        self._collected_namespaces.clear()
        self._auto_prolog_var_names.clear()

    def collect_config_vars(self, owner: type, ns: tuple[str, ...] = ()) -> None:
        """Entry point to collect ConfigVar from a class and its nested NameSpace classes.
        Get the attributes and merge them.
        """

        conf_var_list: Iterable[ConfigVar]
        if "_config_var_defs" in owner.__dict__:
            # If the owner class already has _config_var_defs, it means it's a subclass of AppConfig and we can just
            # merge its _config_var_defs
            inner_list = cast(
                dict[str, ConfigVar], getattr(owner, "_config_var_defs", None)
            )
            assert inner_list is not None
            conf_var_list = inner_list.values()
        else:
            # If the owner class doesn't have _config_var_defs, it means it's not a subclass of AppConfig and we need to scan it for ConfigVarDef attributes
            conf_var_list = self._extract_config_vars(owner, ns=ns)

        self._merge_config_var_defs(conf_var_list)

    def _extract_config_vars(
        self, owner: type, ns: tuple[str, ...] = ()
    ) -> Iterable[ConfigVar]:
        """
        Search for ConfigVar defined in the owner class and its nested NameSpace classes.

        loop over the class __dict__ to find ConfigVar defined in this class and yield them.

        If a ConfigVar doesn't have help defined we try to attach the docstring of the attribute as help if it exists.

        Add its namespace to the ConfigVar instance.
        Add its _fq_name to the ConfigVar instance.
        """
        # extract docstrings from the class source code
        docstrings = get_attribute_docstrings(owner)

        # Inspect the class __dict__ to find ConfigVar defined in this class
        for attr_name, value in owner.__dict__.items():
            if isinstance(value, ConfigVar):
                # If help is missing, try to use the docstring
                if not value.help and attr_name in docstrings:
                    value.help = docstrings[attr_name]

                name = getattr(value, "name", None)
                if name is None:
                    raise ValueError(
                        f"attribute {attr_name} of class {owner.__name__} is missing attribute 'name' in its ConfigVar, this should never happen."
                    )
                value.fq_name = f"{'.'.join((*ns, name))}"
                value.name_space = ns
                yield value

            elif (
                isinstance(value, type)
                and issubclass(value, NameSpace)
                and value is not NameSpace
            ):
                # Recurse into nested NameSpace classes
                yield from self._extract_config_vars(value, ns=(*ns, attr_name))

    def _merge_config_var_defs(self, new_config_var_defs: Iterable[ConfigVar]) -> None:
        """
        Merge new ConfigVar attributes into the already collected one.

        The merge strategy is explain at the class level docstring
        """
        if not new_config_var_defs:
            return

        for var_def in new_config_var_defs:
            # getting data from var_def and check they are OK.
            var_namespace = cast(tuple[str, ...], getattr(var_def, "name_space", None))
            var_fq_name = cast(Optional[str], getattr(var_def, "fq_name", None))
            if var_namespace is None or var_fq_name is None:
                raise ValueError(
                    f"ConfigVar {var_def.fq_name} is missing _name_space attribute, this should never happen."
                )
            var_fq_name_tuple = tuple(var_fq_name.split("."))

            # first we check if we are masking a namespace.
            if var_fq_name_tuple in self._collected_namespaces.keys():
                # we are masking a namespace, we need to remove all the variables defined in this namespace and its sub-namespaces
                ns_to_remove = []
                for ns in self._collected_namespaces.keys():
                    if ns[: len(var_fq_name_tuple)] == var_fq_name_tuple:
                        ns_to_remove.append(ns)

                # remove the variables defined in the masked namespaces
                for ns in ns_to_remove:
                    for var_to_remove in self._collected_namespaces.get(ns, []):
                        self._collected_vars.pop(var_to_remove)
                        if var_to_remove in self._auto_prolog_var_names:
                            self._auto_prolog_var_names.remove(var_to_remove)
                    # removing the name spaces
                    self._collected_namespaces.pop(ns, None)

            # check if the new var_def is already in the collected vars
            if var_fq_name in self._collected_vars:
                # Override previous definition
                self._collected_vars.pop(var_fq_name)
                if var_fq_name and var_def.auto_prolog:
                    self._auto_prolog_var_names.remove(var_fq_name)

            if var_namespace not in self._collected_namespaces:
                # test if we have found a new namespace, note that empty tuple is a valid namespace for root variable.
                self._collected_namespaces[var_namespace] = []

            # the new var_def is added at the end of the list to preserve the order
            self._collected_vars[var_fq_name] = var_def
            self._collected_namespaces[var_namespace].append(var_fq_name)

            auto_prolog = getattr(var_def, "auto_prolog", None)
            if auto_prolog:
                self._auto_prolog_var_names.append(var_fq_name)

    def get_collected_vars(self) -> dict[str, ConfigVar]:
        """
        Get the collected configuration variables.
        """
        return self._collected_vars.copy()

    def get_auto_prolog_var_names(self) -> list[str]:
        """
        Get only the collected configuration variables that have auto_prolog enabled.
        """
        return self._auto_prolog_var_names.copy()


class AppConfigMeta(type):
    """
    Metaclass for AppConfig and its subclasses to handle class initialization.

    Specifically it only defines the __init__() method with the objective to collect configuration variables
    from AppConfig and its subclasses.

    It set the _config_var_defs attribute of the initialized class with as a dictionary that contains all the ConfigVar
    attributes defined in the class and its ancestrors (parents and parents of parents) indexed by their
    fully qualified name (fq_name), which is the name prefixed with its namespaces separated by dots.

    The order of the attributes is the same as the order of their definition in the hierarchy of classes from the deepest
    ancestor first to the class itself last.The order in which the classes are processed follow the MRO order of the classes being initialized.
    In case of conflict the last definition in the MRO order wins, the older one is removed.
    *Note*: if a NameSpace is masked by a non-namespace attribute defined later in the MRO, all the variables defined in this
    namespace and its sub-namespaces are removed.

    It also set the _autoprolog_var_names attribute of the initialized class with the list of the names of the ConfigVar attributes that have auto_prolog enabled,
    this is used to know which variables to put in the prolog processing when calling resolve_vars().

    During the collection the metaclass also attach the to the ConfigVar instance their namespace and their fq_name.
    It also tries to use the attribute docstring as help if the help is missing.

    """

    def __init__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> None:
        """
        Python metaclass method called when a new class belonging to this metaclass is created. It is called on direct subclasses
        and subclasses of those subclasses.

        - Difference between __mro__ and __bases__
            cls.__bases__ : the direct parents (what you wrote in the definition).
            cls.__mro__ : the complete method resolution order (parents + ancestors) in the effective order.

        Synopsis of the collection strategy:

        - We are collecting the ConfigVar attributes defined in the base classes and merge them to create the _config_var_defs
          list of the current class.

        The collection strategy is the following:
        - If the base has no _config_var_defs attributes it means that it's not a subclass of AppConfig and we need to scan its MRO
          for  searching for ConfigVar attributes defined in its ancestors. In this case we scan all ancestors by starting from
          the deepest ancestor to the closest parent in reversed MRO and we apply the merge strategy described above to merge the
          collected ConfigVar attributes.
        - If a base class has the _config_var_defs attribute it means that it is a subclass of AppConfig and that we have
          already collected its ConfigVar attributes. In this case we can just merge the base class _config_var_defs attribute
          instead of scanning the class again.

        Args:
            name (str): The name of the class being created.
            bases (tuple[type, ...]): A tuple containing the base classes of the class being created.
            attrs (dict[str, Any]): A dictionary containing the class namespace (attributes and methods)
                                  populated during the execution of the class body.
                                  Keys are the names of the attributes (strings) and values are the
                                  attribute values (methods, class variables, properties, etc.).
        """

        super().__init__(name, bases, attrs)

        # we use the ConfigVarCollector to collect the ConfigVar attributes defined in the various classes.
        # It also handle the merge strategy and the namespace masking detection.
        with ConfigVarCollector() as collector:
            for base in reversed(bases):
                if not "_config_var_defs" in cls.__dict__:
                    # If the base class has not a _config_var_defs, it means it's not a subclass of AppConfig and we need
                    # to collect the ConfigVar by scanning its ancestors, note that the first class in _mro_ is the class itself
                    # so we skip and we also skip the last one which is object.
                    for ancestor in reversed(base.__mro__[1:-1]):
                        # we need to detect if we have passed an AppConfig class in the MRO.
                        # to avoid scanning parent that already have their ConfigVar collected and stored in their _config_var_defs attribute.
                        # Problem 1: we can't import AppConfig because we are building it.
                        # Problem 2: the MRO contains also `lateral` classes in case of multiple inheritance so it's not possibl to say that when we
                        #   a processing a class whose name is 'AppConfig' all remaining classes will be subclasses of AppConfig (if they are lateral inheritance)

                        collector.collect_config_vars(ancestor)

                # in all case we need to collect the ConfigVar defined in the base class itself:
                #   - if it has a _config_var_defs attribute the collector we will merge it.
                #   - if it doesn't have a _config_var_defs the collector will collect the ConfigVar defined in its __dict__.
                collector.collect_config_vars(base)

            # finally we collect the ConfigVar defined in the initialized class itself
            collector.collect_config_vars(cls)

            cls._config_var_defs = collector.get_collected_vars()
            cls._autoprolog_var_names = collector.get_auto_prolog_var_names()
