#
# Authors: Robert Abram <robert.abram@entpack.com>
#
# Copyright (C) 2015-2016 EntPack
# see file 'LICENSE' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#


import logging
import os
from collections import OrderedDict

from silentdune_client.utils.misc import determine_config_root, get_active_firewall, rmdir

try:
    from configparser import ConfigParser
except ImportError:
    from ConfigParser import ConfigParser  # ver. < 3.0

_logger = logging.getLogger('sd-client')

# Global debug variable
debug = False


class ClientConfiguration(object):
    _config_root = None
    _config_file = None  # Full path and file name for the configuration file.
    _config_section = 'settings'  # This is the default configuration file section name, do not change.
    _config = OrderedDict()

    def __init__(self, config_file=None):
        """
        Initialize the configuration object with the most reasonable values for the default settings.
        :param config_file: Use alternate configuration file.
        """

        self._config_root = determine_config_root()

        # Set the configuration file path and name.
        if config_file:
            self._config_file = config_file
            self._config_root = os.path.split(config_file)[0]
        else:
            self._config_file = os.path.join(self._config_root, 'sdc.conf')

        # Set the default file paths based on if we are running as root or not.
        if os.getuid() == 0:
            self.set(self._config_section, 'pidfile', '/var/run/silentdune/sdc.pid')
            self.set(self._config_section, 'logfile', '/var/log/silentdune.log')
        else:
            self.set(self._config_section, 'pidfile', os.path.join(self._config_root, 'sdc.pid'))
            self.set(self._config_section, 'logfile', os.path.join(self._config_root, 'silentdune.log'))

        # self.set(self._config_section, 'user', 'silentdune')
        # self.set(self._config_section, 'group', 'silentdune')

        # # Set the previous firewall service to the currently running firewall.
        # self.set(self._config_section, 'previous_firewall_service', get_active_firewall())

        # Set the section heading comments
        self.set_comment(self._config_section, 'settings',
                         _('; Silent Dune Client Configuration File\n'  # noqa
                           '; This file was automatically generated by the installer.\n'
                           '; Run "sdc-install --help" to configure this file.\n'))

        # Set the section item comments
        self.set_comment(self._config_section, 'pidfile', _('; The path and file name for the PID file.\n'))  # noqa
        # self.set_comment(self._config_section, 'user', _('; The local user the service should run as.\n'  # noqa
        #                                                  '; Default: silentdune.\n'))
        # self.set_comment(self._config_section, 'group', _('; The local group the service should run as.\n'  # noqa
        #                                                   '; Default: silentdune.\n'))
        # self.set_comment(self._config_section, 'previous_firewall_service',
        #                  _('; The previous firewall service. Warning: Changing this value may\n'  # noqa
        #                    '; compromise security on this system if the Silent Dune client is\n'
        #                    '; uninstalled.\n'))

    def delete(self, section, key):
        """
        Delete a configuration key from the section specified.
        :param section: Configuration section name
        :param key:
        :return:
        """

        if not section or section not in self._config:
            msg = 'Invalid configuration section name ({0}).'.format(section)
            raise ValueError(msg)

        if not key or key not in self._config[section]:
            msg = 'Invalid configuration key name ({0}).'.format(key)
            raise ValueError(msg)

        del self._config[section][key]

        return True

    def set(self, section, key, val):
        """
        Set a configuration value in the section specified.
        :param section: Configuration section name
        :param key: Value key name
        :param val: Value to set
        :return:
        """
        if not section:
            msg = 'Invalid configuration section name ({0}).'.format(section)
            raise ValueError(msg)

        if section not in self._config:
            self._config[section] = OrderedDict()

        self._config[section][key] = val

    def get(self, section, key):
        """
        Retrieve a configuration value.
        :param section: Configuration section name
        :param key: Value key name
        :return Value or None:
        """
        if not section or section not in self._config:
            msg = 'Invalid configuration section name ({0}).'.format(section)
            raise ValueError(msg)

        if not key or key not in self._config[section]:
            msg = 'Invalid configuration key name ({0}).'.format(key)
            raise ValueError(msg)

        return self._config[section][key]

    def set_comment(self, section, key, comment):
        """
        Set the comments for a given key in the section specified.
        """

        if not section:
            msg = 'Invalid configuration section name ({0}).'.format(section)
            raise ValueError(msg)

        if 'comments' not in self._config[section]:
            self._config[section]['comments'] = OrderedDict()

        self._config[section]['comments'][key] = comment

    def validate_config_file(self):
        """
        Check to see if the configuration file exists, is readable and valid.
        """

        if not os.path.exists(self._config_file):
            _logger.debug('Configuration file ({0}) does not exist.'.format(self._config_file))
            return None

        config = ConfigParser(dict_type=OrderedDict)

        try:
            config.read(self._config_file)
        except ConfigParser.Error:
            _logger.debug('Configuration file is invalid.')
            return None

        return config

    def read_config(self):
        """
        Read the configuration file.
        """
        # Return the saved configuration loaded by validate_config_file().
        return self.validate_config_file()

    def write_config(self):
        """
        Write out the configuration to a file, we do NOT use ConfigParser to write out the config.
        :param config: Must be a ConfigParser object.
        """
        try:

            with open(self._config_file, 'w') as handle:

                # Loop through each section
                for name, section in self._config.items():
                    _logger.debug('Config: writing section: {0}'.format(name))

                    # Write out the section name
                    handle.write('[' + name + ']\n')

                    # Write any section header comments
                    if name in section['comments']:
                        comment = section['comments'][name]
                        handle.write(comment.rstrip() + '\n\n')

                    # Write each section item out
                    for (key, value) in section.items():

                        if key == 'comments':
                            continue

                        _logger.debug('Config: Key {0}={1}'.format(key, value))

                        # Write item comments if found.
                        if key in section['comments']:
                            comment = section['comments'][key]
                            handle.write(comment.rstrip() + '\n')

                        handle.write(key + '=' + str(value) + '\n\n')

            os.chmod(self._config_file, 0o640)
        except IOError:
            return False

        return True

    def create_directories(self):
        """
        Make sure directories used by the client exist and are set to the correct permissions.
        :return:
        """

        pidfile = None

        try:
            # user = self.get(self._config_section, 'user')
            # group = self.get(self._config_section, 'group')
            logfile = self.get(self._config_section, 'logfile')
        except ValueError:
            _logger.error('Unable to retrieve configuration values.')
            return False

        # The pidfile configuration item might not exist.
        try:
            pidfile = self.get(self._config_section, 'pidfile')
        except ValueError:
            pass

        # try:
        #     # Get information about the daemon process user and group.
        #     userinfo = pwd.getpwnam(user)
        #     groupinfo = grp.getgrnam(group)
        # except:
        #     _logger.error('Unable to get information about daemon process user and group.')
        #     return False

        # Create configuration root directory.
        try:
            configpath = self._config_root
            _logger.debug('Creating Configuration path ({0})'.format(configpath))
            if not os.path.exists(configpath):
                os.makedirs(configpath, 0o770)
            # os.chown(configpath, userinfo.pw_uid, groupinfo.gr_gid)
        except:
            _logger.error('Unable to create PID file path.')
            return False

        # If needed, create PID file path.
        try:
            if pidfile:
                pidpath = os.path.split(pidfile)[0]
                _logger.debug('Creating PID path ({0})'.format(pidpath))
                if not os.path.exists(pidpath):
                    os.makedirs(pidpath, 0o770)
                # os.chown(pidpath, userinfo.pw_uid, groupinfo.gr_gid)
        except:
            _logger.error('Unable to create PID file path.')
            return False

        # Create Log file path.
        try:
            logpath = os.path.split(logfile)[0]
            _logger.debug('Creating Log file path ({0})'.format(logpath))
            if not os.path.exists(logpath):
                os.makedirs(logpath, 0o770)
            # os.chown(logpath, userinfo.pw_uid, groupinfo.gr_gid)
        except:
            _logger.error('Unable to create LOG file path.')
            return False

        return True

    def delete_directories(self):
        """
        Remove directories used by the daemon process
        :return:
        """

        result = True
        pidfile = None

        try:
            logfile = self.get(self._config_section, 'logfile')
        except ValueError:
            _logger.error('Unable to retrieve directory configuration values.')
            return False

        # The pidfile configuration item might not exist.
        try:
            pidfile = self.get(self._config_section, 'pidfile')
        except ValueError:
            pass

        # Remove log file directory
        logpath = os.path.split(logfile)[0]
        if os.path.exists(logpath) and os.path.realpath(logpath) != '/':
            try:
                rmdir(logpath)
            except:
                _logger.error('Failed to delete logfile directory ({0}).'.format(logpath))
                result = False

        # Remove PID file directory
        if pidfile:
            pidpath = os.path.split(pidfile)[0]
            if os.path.exists(pidpath) and os.path.realpath(pidpath) != '/':
                try:
                    rmdir(pidpath)
                except:
                    _logger.error('Failed to delete PID file directory ({0}).'.format(pidpath))
                    result = False

        # Remove Configuration path
        if os.path.exists(self._config_root):
            try:
                rmdir(self._config_root)
            except:
                _logger.error('Failed to delete config directory ({0}).'.format(logpath))
                result = False

        return result
