"""Contains the main class to interact with the carconnectivity library."""
from __future__ import annotations
from typing import TYPE_CHECKING

import importlib
import pkgutil

import json
import logging
import os

import carconnectivity_connectors
import carconnectivity_plugins

from carconnectivity.objects import GenericObject
from carconnectivity.garage import Garage
from carconnectivity.util import ExtendedEncoder
from carconnectivity.errors import ConfigurationError
from carconnectivity.connectors import Connectors
from carconnectivity.plugins import Plugins
from carconnectivity.attributes import StringAttribute
from carconnectivity._version import __version__

if TYPE_CHECKING:
    from typing import Dict, Any, Optional, Iterator

    from types import ModuleType

    from carconnectivity_connectors.base.connector import BaseConnector
    from carconnectivity_plugins.base.plugin import BasePlugin

LOG: logging.Logger = logging.getLogger("carconnectivity")


def __iter_namespace(ns_pkg) -> Iterator[pkgutil.ModuleInfo]:
    return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")


discovered_connectors: Dict[str, ModuleType] = {
    name: importlib.import_module('.connector', name)
    for finder, name, ispkg
    in __iter_namespace(carconnectivity_connectors)
}

discovered_plugins: Dict[str, ModuleType] = {
    name: importlib.import_module('.plugin', name)
    for finder, name, ispkg
    in __iter_namespace(carconnectivity_plugins)
}


class CarConnectivity(GenericObject):
    """
    CarConnectivity class is the main class to interact with the carconnectivity library.

    Attributes:
        config (Dict): Configuration dictionary for car connectivity.
        connectors (List[BaseConnector]): List of connector instances.
        plugins (List[BasePlugin]): List of plugin instances.
        garage (Garage): Instance of the Garage class.
    """
    def __init__(self, config: Dict, tokenstore_file: Optional[str] = None, cache_file: Optional[str] = None) -> None:
        """
        Initialize the CarConnectivity object.

        Args:
            config (Dict): Configuration dictionary for car connectivity.
            tokenstore_file (Optional[str]): Path to the token store file. Defaults to None.

        Raises:
            ValueError: If the configuration is invalid.
        """
        super().__init__(object_id='', parent=None)
        self.delay_notifications = True
        self.__cache: Dict[str, Any] = {}
        self.__tokenstore: Dict[str, Any] = {}
        self.__tokenstore_file: Optional[str] = tokenstore_file
        self.__cache_file: Optional[str] = cache_file

        self.config: Dict[Any, Any] = config
        self.connectors: Connectors = Connectors(car_connectivity=self)
        self.plugins: Plugins = Plugins(car_connectivity=self)
        self.garage: Garage = Garage(self)

        self.version: StringAttribute = StringAttribute(name="version", parent=self, value=__version__)

        if 'carConnectivity' not in config:
            raise ConfigurationError("Invalid configuration: 'carConnectivity' is missing")
        # Configure logging
        if 'log_level' in config['carConnectivity'] and config['carConnectivity']['log_level'] is not None:
            config['carConnectivity']['log_level'] = config['carConnectivity']['log_level'].upper()
            if config['carConnectivity']['log_level'] in logging.getLevelNamesMapping():
                LOG.setLevel(config['carConnectivity']['log_level'])
            else:
                raise ConfigurationError(f'Invalid log level: "{config["carConnectivity"]["log_level"]}" not in {list(logging.getLevelNamesMapping().keys())}')
        if 'log_format' in config['carConnectivity'] and config['carConnectivity']['log_format'] is not None:
            log_format: str = config['carConnectivity']['log_format']
        else:
            log_format: str = '%(asctime)s:%(name)s:%(levelname)s:%(module)s:%(message)s'
        formatter = logging.Formatter(log_format)
        if 'log_date_format' in config['carConnectivity'] and config['carConnectivity']['log_date_format'] is not None:
            formatter.datefmt = config['carConnectivity']['log_date_format']
        else:
            formatter.datefmt = '%Y-%m-%dT%H:%M:%S%z'
        for handler in logging.getLogger().handlers:
            handler.setFormatter(formatter)

        if self.__tokenstore_file is not None:
            try:
                with open(file=self.__tokenstore_file, mode='r', encoding='utf8') as file:
                    self.__tokenstore = json.load(file)
            except json.JSONDecodeError as err:
                LOG.info('Could not use tokenstore from file %s (%s)', tokenstore_file, err.msg)
                self.__tokenstore = {}
            except FileNotFoundError as err:
                LOG.info('Could not use tokenstore from file %s (%s)', tokenstore_file, err)
                self.__tokenstore = {}
        else:
            self.__tokenstore = {}

        # Fill Cache
        if self.__cache_file is not None:
            LOG.info('Reading cachefile %s', cache_file)
            try:
                with open(self.__cache_file, 'r', encoding='utf8') as file:
                    self.__cache = json.load(file)
            except json.decoder.JSONDecodeError:
                LOG.error('Cachefile %s seems corrupted will delete it and try to create a new one. '
                          'If this problem persists please check if a problem with your disk exists.', self.__cache_file)
                os.remove(self.__cache_file)
                self.__cache = {}
            except FileNotFoundError:
                self.__cache = {}

        if 'connectors' in config['carConnectivity']:
            for connector_config in config['carConnectivity']['connectors']:
                if 'type' not in connector_config:
                    raise ConfigurationError("Invalid configuration: 'type' is missing in connector")
                if f"carconnectivity_connectors.{connector_config['type']}" not in discovered_connectors:
                    raise ConfigurationError(f"Invalid configuration: connector type '{connector_config['type']}' is not known")
                if 'disabled' in connector_config and connector_config['disabled']:
                    LOG.info('Skipping disabled connector %s', connector_config['type'])
                    continue
                connector_class = getattr(discovered_connectors['carconnectivity_connectors.' + connector_config['type']], 'Connector')
                if 'connector_id' in connector_config and connector_config['connector_id'] is not None:
                    connector_id = connector_config['connector_id']
                else:
                    connector_id = connector_config['type']
                if connector_id in self.connectors.connectors:
                    raise ConfigurationError(f"Invalid configuration: connector '{connector_id}' is not unique, set a 'connector_id' in configuration")
                connector: BaseConnector = connector_class(connector_id=connector_id, car_connectivity=self, config=connector_config['config'])
                self.connectors.connectors[connector_id] = connector
                connector.startup()
        if 'plugins' in config['carConnectivity']:
            for plugin_config in config['carConnectivity']['plugins']:
                if 'type' not in plugin_config:
                    raise ConfigurationError("Invalid configuration: 'type' is missing in plugin")
                if f"carconnectivity_plugins.{plugin_config['type']}" not in discovered_plugins:
                    raise ConfigurationError(f"Invalid configuration: plugin type '{plugin_config['type']}' is not known")
                if 'disabled' in plugin_config and plugin_config['disabled']:
                    LOG.info('Skipping disabled plugin %s', plugin_config['type'])
                    continue
                plugin_class = getattr(discovered_plugins['carconnectivity_plugins.' + plugin_config['type']], 'Plugin')
                if 'plugin_id' in plugin_config and plugin_config['plugin_id'] is not None:
                    plugin_id: str = plugin_config['plugin_id']
                else:
                    plugin_id: str = plugin_config['type']
                if plugin_id in self.plugins.plugins:
                    raise ConfigurationError(f"Invalid configuration: connector '{plugin_id}' is not unique, set a 'connector_id' in configuration")
                plugin: BasePlugin = plugin_class(plugin_id=plugin_id, car_connectivity=self, config=plugin_config['config'])
                self.plugins.plugins[plugin_id] = plugin
                plugin.startup()
        self.delay_notifications = False

    def fetch_all(self) -> None:
        """
        Fetch data from all connectors.

        This method iterates over all connectors in the `self.connectors` list
        and calls their `fetch_all` method to retrieve data.
        """
        for connector in self.connectors.connectors.values():
            connector.fetch_all()

    def persist(self) -> None:
        """
        Persist the token store to a file.

        This method writes the token store to a specified file in JSON format.
        If the token store or the file path is not set, the method does nothing.
        If an error occurs during writing, it logs an error message.
        """
        if self.__tokenstore and self.__tokenstore_file:
            try:
                with open(file=self.__tokenstore_file, mode='w', encoding='utf8') as file:
                    json.dump(self.__tokenstore, file)
                LOG.info('Writing tokenstore to file %s', self.__tokenstore_file)
            except ValueError as err:  # pragma: no cover
                LOG.info('Could not write tokenstore to file %s (%s)', self.__tokenstore_file, err)

        # Persist cache
        if self.__cache and self.__cache_file:
            LOG.info('Writing cachefile %s', self.__cache_file)
            with open(file=self.__cache_file, mode='w', encoding='utf8') as file:
                json.dump(self.__cache, file, cls=ExtendedEncoder)

    def shutdown(self) -> None:
        """
        Shuts down all connectors and persists the current state.

        This method iterates over all connectors in the `self.connectors` list and
        calls their `shutdown` method. After all connectors have been shut down,
        it calls the `persist` method to save the current state.
        """
        for connector in self.connectors.connectors.values():
            connector.shutdown()
        for plugin in self.plugins.plugins.values():
            plugin.shutdown()
        self.persist()

    def get_tokenstore(self) -> Dict[str, Any]:
        """
        Retrieve the token store.

        Returns:
            Dict[str, Any]: A dictionary containing the token store.
        """
        return self.__tokenstore

    def get_cache(self) -> Dict[str, Any]:
        """
        Retrieve the cache.

        Returns:
            Dict[str, Any]: The current cache stored in the object.
        """
        return self.__cache

    def get_garage(self) -> Optional[Garage]:
        """
        Retrieve the garage associated with the car connectivity.

        Returns:
            Garage | None: The garage object if available, otherwise None.
        """
        return self.garage

    def __str__(self) -> str:
        return_string: str = ''
        return_string += str(self.get_garage())
        return return_string
