from __future__ import print_function, division, absolute_import

# Copyright (c) 2016 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#
import dbus
import logging
import six
from iniparse import ini

import rhsm
import rhsm.config
import rhsm.logutil

from subscription_manager.i18n import Locale

from rhsmlib.dbus import constants, base_object, util, dbus_utils
from rhsmlib.services.config import Config
from rhsmlib.file_monitor import CONFIG_WATCHER
from rhsmlib.dbus.server import Server

from dbus import DBusException
log = logging.getLogger(__name__)


class ConfigDBusObject(base_object.BaseObject):
    default_dbus_path = constants.CONFIG_DBUS_PATH
    interface_name = constants.CONFIG_INTERFACE

    def __init__(self, conn=None, object_path=None, bus_name=None, parser=None):
        self.config = Config(parser)
        super(ConfigDBusObject, self).__init__(conn=conn, object_path=object_path, bus_name=bus_name)

    @util.dbus_service_signal(
        constants.CONFIG_INTERFACE,
        signature=''
    )
    @util.dbus_handle_exceptions
    def ConfigChanged(self):
        """
        Signal fired, when config is created/deleted/changed
        :return: None
        """
        log.debug("D-Bus signal %s emitted" % constants.CONFIG_INTERFACE)
        return None

    @util.dbus_service_method(
        constants.CONFIG_INTERFACE,
        in_signature='svs')
    @util.dbus_handle_sender
    @util.dbus_handle_exceptions
    def Set(self, property_name, new_value, locale, sender=None):
        """
        Method used for setting only one value. When more than one value is going to be set, then it is
        strongly recommended to use method SetAll(), because configuration is saved to the configuration
        file at the end of method Set()
        :param property_name: string with property e.g. server.hostname
        :param new_value: string with new value
        :param locale: string with locale
        :param sender: not used
        :return: None
        """
        property_name = dbus_utils.dbus_to_python(property_name, expected_type=str)
        new_value = dbus_utils.dbus_to_python(new_value, expected_type=str)
        locale = dbus_utils.dbus_to_python(locale, expected_type=str)
        Locale.set(locale)

        section, _dot, property_name = property_name.partition('.')

        if not property_name:
            raise DBusException("Setting an entire section is not supported.  Use 'section.property' format.")

        self.config[section][property_name] = new_value

        if section == 'logging':
            logging_changed = True
        else:
            logging_changed = False

        # Try to temporarily disable dir watcher, because 'self.config.persist()' writes configuration
        # file, and it would trigger file system monitor callback function and saved values would be
        # read again. It can cause race conditions, when Set() is called multiple times
        Server.temporary_disable_dir_watchers({CONFIG_WATCHER})

        # Write new config value to configuration file
        self.config.persist()

        # When anything in logging section was just changed, then we have to re-initialize logger
        if logging_changed is True:
            parser = rhsm.config.get_config_parser()
            self.config = Config(parser)
            # The iniparse is not thread-safe and some unexpected exceptions can happen due to
            # race conditions. This is very rare. For this reason we try to catch all exceptions here,
            # because it is not critical.
            # See BZ: 2076948, 2093883
            try:
                rhsm.logutil.init_logger(parser)
            except Exception as err:
                log.warning(f"Re-initialization of logger failed: {err}")

        # Enable watchers after re-initialization of logger to minimize race conditions
        Server.enable_dir_watchers({CONFIG_WATCHER})

    @util.dbus_service_method(
        constants.CONFIG_INTERFACE,
        in_signature='a{sv}s')
    @util.dbus_handle_sender
    @util.dbus_handle_exceptions
    def SetAll(self, configuration, locale, sender=None):
        """
        Method for setting multiple configuration options. Of course all of them could be set.
        :param configuration: d-bus dictionary with configuration. Keys have to include section. e.g.
                              server.hostname. Configuration file is saved to the file at the end of method.
        :param locale: string with locale
        :param sender: not used
        :return: None
        """
        configuration = dbus_utils.dbus_to_python(configuration, expected_type=dict)
        locale = dbus_utils.dbus_to_python(locale, expected_type=str)
        Locale.set(locale)

        log.debug('Setting new configuration values: %s' % str(configuration))

        logging_changed = False

        for property_name, new_value in configuration.items():
            section_name, _dot, property_name = property_name.partition('.')

            if not property_name:
                raise DBusException("Setting an entire section is not supported.  Use 'section.property' format.")

            self.config[section_name][property_name] = new_value

            if section_name == 'logging':
                logging_changed = True

        # Try to temporarily disable dir watcher, because 'self.config.persist()' writes configuration
        # file, and it would trigger file system monitor callback function and saved values would be
        # read again. It can cause race conditions, when SetAll() is called multiple times
        Server.temporary_disable_dir_watchers({CONFIG_WATCHER})

        # Write new config value to configuration file
        self.config.persist()

        # When anything in logging section was just changed, then we have to re-initialize logger
        if logging_changed is True:
            parser = rhsm.config.get_config_parser()
            self.config = Config(parser)
            # The iniparse is not thread-safe and some unexpected exceptions can happen due to
            # race conditions. This is very rare. For this reason we try to catch all exceptions here,
            # because it is not critical.
            # See BZ: 2076948, 2093883
            try:
                rhsm.logutil.init_logger(parser)
            except Exception as err:
                log.warning(f"Re-initialization of logger failed: {err}")

        # Enable watchers after re-initialization of logger to minimize race conditions
        Server.enable_dir_watchers({CONFIG_WATCHER})

    @util.dbus_service_method(
        constants.CONFIG_INTERFACE,
        in_signature='s',
        out_signature='a{sv}')
    @util.dbus_handle_sender
    @util.dbus_handle_exceptions
    def GetAll(self, locale, sender=None):
        """
        Method for getting whole configuration
        :param locale: string with locale
        :param sender: not used
        :return: D-bus dictionary with configuration
        """
        locale = dbus_utils.dbus_to_python(locale, expected_type=str)
        Locale.set(locale)

        d = dbus.Dictionary({}, signature='sv')
        for k, v in six.iteritems(self.config):
            d[k] = dbus.Dictionary({}, signature='ss')
            for kk, vv in six.iteritems(v):
                d[k][kk] = vv

        return d

    @util.dbus_service_method(
        constants.CONFIG_INTERFACE,
        in_signature='ss',
        out_signature='v')
    @util.dbus_handle_sender
    @util.dbus_handle_exceptions
    def Get(self, property_name, locale, sender=None):
        """
        D-Bus method for getting one configuration property or one section
        :param property_name: string with name of property e.g. server.hostname or section e.g. server
        :param locale: string with locale
        :param sender: not used
        :return: string with value of property or dictionary with dictionary of one section
        """
        locale = dbus_utils.dbus_to_python(locale, expected_type=str)
        Locale.set(locale)

        section, _dot, property_name = property_name.partition('.')

        if property_name:
            return self.config[section][property_name]
        else:
            return dbus.Dictionary(self.config[section], signature='sv')

    def reload(self):
        """
        This callback method is called, when i-notify or periodical directory polling detects
        any change of rhsm.conf file. Thus, configuration file is reloaded and new values are used.
        """
        parser = rhsm.config.get_config_parser()

        # We are going to read configuration file again, but we have to clean all data in parser object
        # this way, because iniparse module doesn't provide better method to do that.
        parser.data = ini.INIConfig(None, optionxformsource=parser)

        # We have to read parser again to get fresh data from the file
        files_read = parser.read()

        if len(files_read) > 0:
            log.debug('files read: %s' % str(files_read))
            self.config = Config(parser)
            rhsm.logutil.init_logger(parser)
            log.debug("Configuration file: %s reloaded: %s" % (parser.config_file, str(self.config)))
        else:
            log.warning("Unable to read configuration file: %s" % parser.config_file)
