# -*- coding: utf-8 -*-
# vim:fenc=utf-8

# Copyright (C) 2012-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
#
# X2Go Session Broker is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# X2Go Session Broker 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

"""\
:class:`x2gobroker.brokers.inifile_broker.X2GoBroker` class - a simple X2GoBroker implementations that uses text-based config files (also supports load balancing)

"""
__NAME__ = 'x2gobroker-pylib'

# modules
import copy
import netaddr
import re

# Python X2GoBroker modules
import x2gobroker.brokers.base_broker as base
import x2gobroker.config
import x2gobroker.defaults
import x2gobroker.x2gobroker_exceptions

from configparser import NoSectionError

class X2GoBroker(base.X2GoBroker):
    """\
    :class:`x2gobroker.brokers.inifile_broker.X2GoBroker` implements a broker backend
    retrieving its session profile and ACL configuration from a file in
    INI file format.

    """

    backend_name = 'inifile'

    def __init__(self, profile_config_file=None, profile_config_defaults=None, **kwargs):
        """\
        Initialize a new INI file based X2GoBroker instance to control
        X2Go session through an X2Go Client with an intermediate session
        broker

        :param profile_config_file: path to the backend's session profile configuration (x2gobroker-sessionprofiles.conf)
        :type profile_config_file: ``str``
        :param profile_config_defaults: Default settings for session profile configuration parameters.
        :type profile_config_defaults: ``dict``

        """
        base.X2GoBroker.__init__(self, **kwargs)

        if profile_config_file is None: profile_config_file = x2gobroker.defaults.X2GOBROKER_SESSIONPROFILES
        if profile_config_defaults is None: profile_config_defaults = x2gobroker.defaults.X2GOBROKER_SESSIONPROFILE_DEFAULTS
        self.session_profiles = x2gobroker.config.X2GoBrokerConfigFile(config_files=profile_config_file, defaults=profile_config_defaults)

    def get_profile_ids(self):
        """\
        Retrieve the complete list of session profile IDs.

        With the ``inifile`` broker backend, the profile IDs are the
        names of the INI file's sections.

        :returns: list of profile IDs
        :rtype: ``list``

        """
        return self.session_profiles.list_sections()

    def get_profile_defaults(self):
        """\
        Get the session profile defaults, i.e. profile options that all
        configured session profiles have in common.

        The defaults are hard-coded in :mod:`x2gobroker.defaults` for class
        :class:`x2gobroker.brokers.base_broker.X2GoBroker`. With the ``inifile``
        backend, they can be overridden/customized under the INI file's
        ``[DEFAULT]`` section.

        :returns: a dictionary containing the session profile defaults
        :rtype: ``dict``

        """
        profile_defaults = self.session_profiles.get_defaults()
        for key in list(profile_defaults.keys()):
            if key.startswith('acl-'):
                del profile_defaults[key]
            elif type(profile_defaults[key]) == list:
                profile_defaults.update({ key: [ v for v in profile_defaults[key] if v ] })

        return profile_defaults

    def get_profile(self, profile_id):
        """\
        Get the session profile for profile ID <profile_id>.

        With the ``inifile`` broker backend, the session profile
        parameters are the given ``<parameter>=<value>`` pairs under the section
        ``[<profile_id>]``.

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the session profile for ID <profile_id>
        :rtype: ``dict``

        """
        try:
            profile = self.session_profiles.get_section(profile_id)
        except NoSectionError:
            raise x2gobroker.x2gobroker_exceptions.X2GoBrokerProfileException('No such session profile ID: {profile_id}'.format(profile_id=profile_id))

        profile_defaults = self.get_profile_defaults()
        for key in list(profile_defaults.keys()):
            if key not in list(profile.keys()):
                profile.update({ key: profile_defaults[key] })
            if type(profile_defaults[key]) == list:
                profile.update({ key: [ v for v in profile[key] if v ] })

        for key in list(profile.keys()):
            if key.startswith('acl-'):
                del profile[key]
            if key.startswith('broker-'):
                del profile[key]
            if key == 'default':
                del profile[key]
            if key == 'host':
                _hosts = copy.deepcopy(profile[key])
                try: _default_sshport = int(profile['sshport'])
                except TypeError: _default_sshport = 22
                profile[key] = []
                for host in _hosts:
                    if re.match('^.*\ \(.*\)$', host):
                        _hostname = host.split(' ')[0]
                        _address = host.split(' ')[1][1:-1]
                        _address, _port = x2gobroker.utils.split_host_address(_address, default_port=_default_sshport)

                        # test if _address is a valid hostname, a valid DNS name or an IPv4/IPv6 address
                        if (re.match('(?!-)[A-Z\d-]{1,63}(?<!-)$', _hostname, flags=re.IGNORECASE) or \
                            re.match('(?=^.{1,254}$)(^(?:(?!\d|-)[a-zA-Z0-9\-]{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)', _hostname, flags=re.IGNORECASE)) and \
                           (re.match('(?=^.{1,254}$)(^(?:(?!\d|-)[a-zA-Z0-9\-]{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)', _address, flags=re.IGNORECASE) or \
                            netaddr.valid_ipv4(_address) or netaddr.valid_ipv6(_address)):

                            profile["host={hostname}".format(hostname=_hostname)] = _address
                            if _port != _default_sshport:
                                profile["sshport={hostname}".format(hostname=_hostname)] = _port

                        profile[key].append(_hostname)
                    else:
                        profile[key].append(host)

        return profile

    def get_profile_broker(self, profile_id):
        """\
        Get broker-specific session profile options from the session profile with profile ID <profile_id>.

        With the ``inifile`` broker backend, these broker specific
        options are ``<param>=<value>`` pairs prefixed like this:
        ``broker-<param>=<value>``

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the session profile for ID <profile_id>
        :rtype: ``dict``

        """
        profile = self.session_profiles.get_section(profile_id)
        for key in list(profile.keys()):
            if not key.startswith('broker-'):
                del profile[key]
            if key.startswith('broker-') and (profile[key] == '' or profile[key] == ['']):
                del profile[key]
        return profile

    def get_profile_acls(self, profile_id):
        """\
        Get the ACLs for session profile with profile ID <profile_id>.

        With the ``inifile`` broker backend, these ACL specific options
        are ``<param>=<value>`` pairs prefixed like this:
        ``acl-<param>=<value>``

        :param profile_id: the ID of a profile
        :type profile_id: ``str``

        :returns: a dictionary representing the ACLs for session profile with ID <profile_id>
        :rtype: ``dict``

        """
        profile = self.session_profiles.get_section(profile_id)
        for key in list(profile.keys()):
            if not key.startswith('acl-'):
                del profile[key]
            if key.startswith('acl-') and (profile[key] == '' or profile[key] == ['']):
                del profile[key]
        return profile
