"""This module provides utilities for managing configurations."""

# Import standard modules
from pathlib import Path
from string import Template
from typing import Optional
from xml.etree.ElementTree import ParseError

# Import internal modules
from .data import DataError, DataSource, SourceType
from .lang import BatCaveError, BatCaveException, PathName


class ConfigurationError(BatCaveException):
    """Configuration Exceptions.

    Attributes:
        BAD_FORMAT: The configuration file format is invalid.
        BAD_SCHEMA: The configuration schema is not supported.
        CONFIG_NOT_FOUND: The specified configuration file was not found.
    """
    BAD_FORMAT = BatCaveError(1, Template('Bad format for configuration file: $file'))
    BAD_SCHEMA = BatCaveError(2, Template('Invalid schema in configuration file: $file'))
    CONFIG_NOT_FOUND = BatCaveError(3, Template('Unable to find the configuration file: $file'))


class ConfigCollection:
    """This is a container class to hold a collection of configurations read from a file.

    Attributes:
        INCLUDE_CONFIG_TAG: The configuration tag which indicates an include file.
        _CURRENT_CONFIG_SCHEMA: The default DataSource schema to use.
        _MASK_MISSING: The parameter to indicate that child configurations should be ignored.
        _PARAMS_CONFIGURATION: The configuration section which is used as configuration parameters for the collection.
        _PARENT_CONFIGURATION: The parameter to indicate the parent configuration name.
    """
    _CURRENT_CONFIG_SCHEMA = 1
    _MASK_MISSING = 'mask_missing'
    _PARAMS_CONFIGURATION = 'configuration'
    _PARENT_CONFIGURATION = 'parent'

    INCLUDE_CONFIG_TAG = 'include'

    def __init__(self, name: PathName, /, *, create: bool = False, suffix: str = '_config.xml'):
        """
        Args:
            name: The configuration collection name.
            create (optional, default=False): If True the configuration file will be created if it doesn't exist.
            suffix (optional, default=_config.xml): The suffix to add to the name to derive the configuration file name.

        Attributes:
            params: This is the list of configuration parameters read from the configuration section.
            parent: The parent configuration.
            _config_filename: The derived name of the configuration file.
            _configs: This is the collection of configurations.
            _current: This is the value used by the iterator.
            _data_source: A reference to the DataSource instance read from the configuration file.
            _mask_missing: This value is read from the configuration section and
                if True will prevent missing values from the parent from creating values.
            _name: The value of the name argument.

        Raises:
            ConfigurationError.BAD_FORMAT: The the format of the configuration file is not valid.
            ConfigurationError.BAD_SCHEMA: If the schema of the configuration file is not supported.
            ConfigurationError.CONFIG_NOT_FOUND: If the derived configuration file is not found.
        """
        self._name = (path_name := Path(name)).name
        self._config_filename = path_name.parent / (path_name.name + suffix)
        try:
            self._data_source = DataSource(SourceType.xml, self._config_filename, name=self.name, schema=self._CURRENT_CONFIG_SCHEMA, create=create)
        except DataError as err:
            match err.code:
                case DataError.FILE_OPEN.code:
                    raise ConfigurationError(ConfigurationError.CONFIG_NOT_FOUND, file=self._config_filename) from err
                case DataError.BAD_SCHEMA.code:
                    raise ConfigurationError(ConfigurationError.BAD_SCHEMA, file=self._config_filename) from err
                case _:
                    raise
        except ParseError as err:
            raise ConfigurationError(ConfigurationError.BAD_FORMAT, file=self._config_filename) from err

        self.parent: Optional[ConfigCollection] = None
        self._mask_missing = True
        self.params = getattr(self, self._PARAMS_CONFIGURATION) if hasattr(self, self._PARAMS_CONFIGURATION) else None
        if hasattr(self.params, self._PARENT_CONFIGURATION):
            self.parent = ConfigCollection(getattr(self.params, self._PARENT_CONFIGURATION))
        self._mask_missing = hasattr(self.params, self._MASK_MISSING)

        self._configs = [getattr(self, t.name) for t in self._data_source.get_tables() if t.name not in (DataSource.INFO_TABLE, self._PARAMS_CONFIGURATION)]
        config_names = [c.name for c in self._configs]
        if self.parent and not self._mask_missing:
            self._configs += [c for c in self.parent if c.name not in config_names]
        self._current = 0

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        self.close()
        return False

    def __getattr__(self, attr: str):
        if self._data_source.has_table(attr):
            parent_config = getattr(self.parent, attr) if (self.parent and hasattr(self.parent, attr)) else None
            config = Configuration(self._data_source, attr, parent=parent_config)
            if hasattr(config, self.INCLUDE_CONFIG_TAG):
                config = Configuration(self._data_source, attr, parent=parent_config, include=getattr(self, getattr(config, self.INCLUDE_CONFIG_TAG)))
            return config
        if self.parent and not self._mask_missing:
            return getattr(self.parent, attr)
        raise AttributeError(f'Unknown configuration ({attr}) in {self._config_filename}')

    def __iter__(self):
        return self

    def __next__(self):
        if self._current >= len(self._configs):
            raise StopIteration()
        self._current += 1
        return self._configs[self._current - 1]

    name = property(lambda s: s._name, doc='A read-only property which returns the name of the configuration.')

    def add(self, name: str, /) -> str:
        """Add an item to the configuration collection.

        Args:
            name: The name of the item to add.

        Returns:
            The value of the added item.
        """
        self._data_source.add_table(name).add_row()
        self._data_source.commit()
        return getattr(self, name)

    def close(self) -> None:
        """Close the underlying data source."""
        self._data_source.close()


class Configuration:
    """This is a container class to hold an individual configuration in a collection."""

    def __init__(self, config_source: DataSource, name: str, /, *, parent: Optional['Configuration'] = None, include: Optional['Configuration'] = None):
        """
        Args:
            config_source: The configuration source.
            name: The configuration name.
            parent (optional, default=None): If not None, the parent configuration.
            include (optional, default=None): If not None, the configuration to include.

        Attributes:
            _data_source: The value of the config_source argument.
            _data_table: The DataTable holding the configuration values for this configuration.
            _include: The value of the include argument.
            _name: The value of the name argument.
            _parent: The value of the parent argument.
        """
        self._name = name
        self._data_source = config_source
        self._data_table = self._data_source.get_table(self._name)
        self._parent = parent
        self._include = include

    def __enter__(self):
        return self

    def __exit__(self, *exc_info):
        return False

    def __getattr__(self, attr: str) -> str:
        # First check this configuration to see if it has the requested attribute
        if values := self._data_table.get_rows(attr):
            return values[0].get_value(attr)

        # Next check the include configuration without considering its parents
        if self._include:
            include_parent_value = self._include._parent  # pylint: disable=protected-access
            self._include._parent = None  # pylint: disable=protected-access
            if hasattr(self._include, attr):
                self._include._parent = include_parent_value  # pylint: disable=protected-access
                return getattr(self._include, attr)
            self._include._parent = include_parent_value  # pylint: disable=protected-access

        # Next check the parent configuration considering its includes
        if self._parent and attr != ConfigCollection.INCLUDE_CONFIG_TAG:
            if hasattr(self._parent, attr):
                return getattr(self._parent, attr)

        # Finally check the include configuration considering its parents
        if self._include:
            if hasattr(self._include, attr):
                return getattr(self._include, attr)

        # The attribute wasn't found
        raise AttributeError(f'Unknown parameter ({attr}) for configuration: {self._name}')

    def __setattr__(self, attr: str, value: str) -> None:
        if attr.startswith('_'):
            super().__setattr__(attr, value)
            return
        self._data_table.get_rows()[0].setvalue(attr, value)
        self._data_source.commit()

    name = property(lambda s: s._name, doc='A read-only property which returns the name of the configuration.')
