from __future__ import absolute_import

import logging
import os

import ldap
import ldapurl
from ldap.controls.libldap import SimplePagedResultsControl
from ldap.controls.sss import SSSRequestControl

from lib import TrueValues
from lib.connector import AuthenticationError
from lib.error import ConfigError

LOG = logging.getLogger("ext/ldap")  # pylint:disable=invalid-name


class LdapConnection(object):
    @classmethod
    def clean_record(cls, record):
        clean_record = {}
        for key, value in record.items():
            try:
                if key == 'memberOf':
                    clean_record[key] = value
                else:
                    if isinstance(value, list):
                        if value:
                            value = value[0]
                        else:
                            value = u""
                    clean_record[key] = value.decode('unicode_escape').encode('iso8859-1').decode('utf8')
            except ValueError:
                clean_record[key] = "*BINARY*"
            except AttributeError:
                clean_record[key] = repr(value)
        return clean_record

    def __init__(self, settings, fields):
        self.pg_ctrl = None
        self.settings = settings
        self.ldap_connection = None
        self.ldap_query_fields = fields

    def authenticate(self):
        # ldap.set_option(ldap.OPT_DEBUG_LEVEL, 1)
        ldap.set_option(ldap.OPT_REFERRALS, 0)
        ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 30)

        # the default LDAP protocol version - if not recognized - is v3
        if self.settings['protocol_version'] == '2':
            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
        else:
            if self.settings['protocol_version'] != '3':
                LOG.warning("Unrecognized Protocol Version '%s', setting to '3'.", self.settings['protocol_version'])
                self.settings['protocol_version'] = '3'
            ldap.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)

        try:
            parsed_url = ldapurl.LDAPUrl(self.settings['url'])
        except ValueError:
            raise AuthenticationError("Invalid url to LDAP service. "
                                      "Check config examples at https://github.com/Oomnitza/oomnitza-connector")
        self.ldap_connection = ldap.initialize(parsed_url.unparse())

        cacert_file = self.settings.get('cacert_file', '')
        if cacert_file:
            cacert_file = os.path.abspath(cacert_file)
            if not os.path.isfile(cacert_file):
                raise ConfigError("%s is not a valid file!" % cacert_file)
            LOG.info("Setting CACert File to: %r.", cacert_file)
            ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, cacert_file)
        cacert_dir = self.settings.get('cacert_dir', '')
        if cacert_dir:
            cacert_dir = os.path.abspath(cacert_dir)
            if not os.path.isdir(cacert_dir):
                raise ConfigError("%s is not a valid directory!" % cacert_dir)
            LOG.info("Setting CACert Dir to: %r.", cacert_dir)
            ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, cacert_dir)

        # check for tls
        # if self.settings['enable_tls'] in self.TrueValues and self.settings['protocol_version'] == '3':
        if self.settings.get('verify_ssl', True) in TrueValues:
            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
        else:
            LOG.warning("verify_ssl = '%s' so SSL certificate validation has been disabled.", self.settings.get('verify_ssl', True))
            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)

        try:
            if self.settings['username'].lower() == 'anonymous':
                self.ldap_connection.simple_bind_s()
            else:
                password = self.settings['password']
                if not password:
                    LOG.warning("No password set for LDAP. Connecting without password.")
                    password = u""

                self.ldap_connection.simple_bind_s(self.settings['username'], password)
        except ldap.INVALID_CREDENTIALS:
            LOG.exception("Error calling simple_bind_s()")
            raise AuthenticationError("Cannot connect to the LDAP server with given credentials. "
                                      "Check the 'username', 'password' and 'dn' options "
                                      "in the config file in the '[ldap]' section.")
        except ldap.UNWILLING_TO_PERFORM as exp:
            LOG.exception("Error calling simple_bind_s()")
            raise AuthenticationError("Cannot connect to the LDAP server with given credentials: " + exp.args[0]['info'])

    def load_data(self, options):
        save_data = self.settings.get("__save_data__", False)
        if save_data:
            options['full_record'] = True

        if self.settings['protocol_version'] == '2':
            if self.settings['groups_dn']:
                users = self.query_groups(options)
            elif self.settings['group_dn']:
                users = self.query_group(options, self.settings['group_dn'])
            else:
                users = self.query_objects(options)
        else:
            if self.settings['groups_dn']:
                users = self.query_groups(options)
            elif self.settings['group_dn']:
                users = self.query_group_paged(options, self.settings['group_dn'])
            else:
                users = self.query_objects_paged(options)

        if save_data:
            data = []
            keys = set()
            for user in users:
                # Note: Not all user dicts contain all the fields. So, need to loop over
                #       all the users to make sure we don't miss any fields.
                keys.update(user.keys())
                data.append(user)

            users = data

        for user in users:
            yield user

    def query_objects(self, options):
        """
        Connects to LDAP server and attempts to query and return all users.
        """
        # search the server for users
        full_record = options.get('full_record', False)
        fields = self.ldap_query_fields
        if full_record:
            fields = None

        ldap_users = self.ldap_connection.search_s(
            self.settings['base_dn'], ldap.SCOPE_SUBTREE, self.settings['filter'],
            fields
        )
        # disconnect and return results
        self.ldap_connection.unbind_s()
        for user in ldap_users:
            if user[0] and user[1]:
                yield self.clean_record(user[1])

    def get_page(self, page_size, full_record):
        """
        Method used to retrieve all the objects in the "page" using Simple Paged Results Manipulation
        and Server Side Sorting extensions

        https://tools.ietf.org/html/rfc2696
        https://tools.ietf.org/html/rfc2891

        :param full_record: flag identifying that we hav to extract the full set of fields
        :param page_size: size of page
        :return: generator
        """
        fields = self.ldap_query_fields
        if full_record:
            fields = None

        # set ldap control extensions
        if not self.pg_ctrl:
            self.pg_ctrl = SimplePagedResultsControl(True, page_size, '')

        serverctrls = [self.pg_ctrl]
        if fields:
            # if we have defined set of fields, use the last one for the sorting
            serverctrls.append(SSSRequestControl(ordering_rules=[fields[-1]]))

        msgid = self.ldap_connection.search_ext(
            self.settings['base_dn'], ldap.SCOPE_SUBTREE, self.settings['filter'],
            fields,
            serverctrls=serverctrls
        )

        _, records, msgid, serverctrls = self.ldap_connection.result3(msgid)
        self.pg_ctrl.cookie = [_ for _ in serverctrls if _.controlType == SimplePagedResultsControl.controlType][0].cookie

        if not self.pg_ctrl.cookie:
            # disconnect, the page is the last one
            self.ldap_connection.unbind_s()

        return (self.clean_record(record[1]) for record in records if (record[0] and record[1]))

    def pages(self, options):
        """
        Gather all the pages and then return them.
        This is not optimal approach but allow us to not handle connection timeout exception
        in case of long data pushing to Oomnitza
        :param options:
        :return:
        """
        pages = []

        page_size = options.get('page_size', 500)
        full_record = options.get('full_record', False)

        LOG.info("Gathering LDAP info...")
        counter = 1
        while not self.pg_ctrl or self.pg_ctrl.cookie:

            pages.append(self.get_page(page_size, full_record))
            LOG.info("Gathered up to %d records" % (counter * page_size))
            counter += 1

        return pages

    def query_objects_paged(self, options):
        """
        Generator over the gathered pages
        :param options:
        :return:
        """
        pages = self.pages(options)

        while pages:

            page_generator = pages.pop()

            for record in page_generator:
                yield record

    def query_groups(self, options):
        members = []
        groups_dn = self.settings['groups_dn']
        for group_dn in groups_dn:
            members.extend(self.query_group(options, group_dn))
        return members

    def query_group(self, options, group_dn):
        """
        Connects to LDAP server and attempts to query and return all objects in a Group.
        """
        group = self.ldap_connection.search_s(
            group_dn, ldap.SCOPE_SUBTREE, "(objectClass=*)", None
        )
        if group:
            group = group[0]
        if len(group) == 2 and group[0] and group[1]:
            group = group[1]

        members = []
        for member in group[self.settings['group_members_attr']]:
            if self.settings['group_member_filter']:
                user = self.get_object(
                    options,
                    filter=self.settings['group_member_filter'].format(member)
                )
            else:
                user = self.get_object(options, dn=member)
            if user:
                members.append(self.clean_record(user))

        return members

    def query_group_paged(self, options, group_dn):
        return self.query_group(options, group_dn)

    def get_object(self, options, dn=None, filter=None):
        full_record = options.get('full_record', False)

        fields = self.ldap_query_fields
        if full_record:
            fields = None

        if dn:
            result = self.ldap_connection.search_s(
                dn, ldap.SCOPE_BASE, self.settings['filter'], fields
            )
        elif filter:
            ldap_filter = '(&{}{})'.format(self.settings['filter'], filter)
            result = self.ldap_connection.search_s(
                self.settings['base_dn'],
                ldap.SCOPE_SUBTREE,
                ldap_filter,
                fields
            )
        else:
            LOG.error("Neither dn nor filter was provided for get_object method")
            return None

        if result:
            if len(result[0]) == 2 and result[0][1]:
                return result[0][1]

        LOG.warning("Unable to get LDAP object for '%s'.", dn)
        return None
