def __init__(self, insightlyUpdater, args):
     """Initialize instance."""
     self.mailer = CannedMailer(args)
     self.updater = insightlyUpdater
class LDAPUpdater:

    """Update LDAP server to represent identity and membership relations stated on Insightly.

    Attributes:
        SDA: Constant representing the name of the SDA category on Insightly.
        FPA: Constant representing the name of the FPA category on Insightly.
        FPA_CRA: Constant representing the name of the FPA with CRA category on Insightly.
        OS_TENANT: Constant representing the name of the OpenStack tenant category on Insightly.
        PIPELINE_NAME: Constant representing the name of the Project execution pipeline on Insightly.

        ACTION_CREATE: Constant representing the create key for the Action function.
        ACTION_DELETE: Constant representing the delete key for the Action function.
        ACTION_UPDATE: Constant representing the update key for the Action function.
    """

    SDA = 'SDA'
    FPA = 'FPA'
    FPA_CRA = 'FPA (CRA)'
    OS_TENANT = 'OpenStack Tenant'
    PIPELINE_NAME = 'Project execution'

    ACTION_CREATE = 'create'
    ACTION_DELETE = 'delete'
    ACTION_UPDATE = 'update'

    _LDAP_TREE = {'accounts': "ou=accounts,dc=forgeservicelab,dc=fi",
                  'projects': "ou=projects,dc=forgeservicelab,dc=fi",
                  'admins': "cn=ldap_admins,ou=roles,dc=forgeservicelab,dc=fi"}

    _PROTECTED_ACCOUNTS = ['admin', 'binder', 'pwdchanger', 'syncer']

    _ALL_OTHER_GROUPS_FILTER = '(&(|(objectClass=groupOfNames)\
                                    (objectClass=groupOfUniqueNames))\
                                  (|(member=cn={user_cn},%(s)s)\
                                    (uniqueMember=cn={user_cn},%(s)s))\
                                  (!(cn:dn:={project_cn})))'.replace(' ', '') % {'s': _LDAP_TREE['accounts']}

    _PLACEHOLDER_NAME = 'FirstName'
    _PLACEHOLDER_SN = 'LastName'

    def __init__(self, insightlyUpdater, args):
        """Initialize instance."""
        self.mailer = CannedMailer(args)
        self.updater = insightlyUpdater

    def _parseName(self, name):
        """Return the first element of a compound name that is not a known particle.

        Args:
            name (str): The name to be parsed.

        Returns:
            str: The transliterated first non-particle element of a name, capped to 10 characters.
        """
        PARTICLES = ['de', 'della', 'von', 'und']
        SPECIAL_CHARS = ['\'', '.', '!']

        splitName = reduce(list.__add__, map(lambda n: n.split('-'), name.split()))

        try:
            while splitName[0].lower() in PARTICLES:
                splitName.pop(0)
        except IndexError:
            pass

        return unidecode(filter(lambda c: c not in SPECIAL_CHARS,
                                splitName[0].decode('utf-8').lower()[:10])) if splitName else None

    def _ldapCN(self, userID, ldap_conn):
        return ldap_conn.ldap_search(self._LDAP_TREE['accounts'], _ldap.SCOPE_ONELEVEL,
                                     filterstr='employeeNumber=%s' % userID,
                                     attrsonly=1)[0][0]

    def _createCN(self, user, ldap_conn):
        firstName = None if user['givenName'] is self._PLACEHOLDER_NAME else self._parseName(user['givenName'])
        lastName = None if user['sn'] is self._PLACEHOLDER_SN else self._parseName(user['sn'])

        cn = '.'.join(filter(lambda n: n, [firstName, lastName]))

        suffix = 0
        while ldap_conn.ldap_search('cn=%s,%s' % (cn, self._LDAP_TREE['accounts']), _ldap.SCOPE_BASE, attrsonly=1):
            cn = '%s.%s' % (cn[:-2], suffix)
            suffix += 1

        return cn

    def _disableAndNotify(self, dn, ldap_conn):
        account = ldap_conn.ldap_search(dn, _ldap.SCOPE_BASE, attrlist=['employeeType', 'cn', 'mail'])[0][1]
        if account and ('employeeType' not in account or not extractOne(account['employeeType'][0],
                                                                        ['disabled'], score_cutoff=80)):
            ldap_conn.ldap_update(dn, [(_ldap.MOD_REPLACE, 'employeeType', 'disabled')])
            map(lambda e: self.mailer.sendCannedMail(e, self.mailer.CANNED_MESSAGES['disabled_account'],
                                                     account['cn'][0]), account['mail'])

    def _pruneAccounts(self, ldap_conn):
        # Disable orphans
        map(lambda entry: self._disableAndNotify(entry, ldap_conn),
            map(lambda dn: dn[0],
                filter(lambda a: 'memberOf' not in a[1].keys() and not any(cn in a[0] for cn in
                                                                           self._PROTECTED_ACCOUNTS),
                       ldap_conn.ldap_search(self._LDAP_TREE['accounts'],
                                             _ldap.SCOPE_ONELEVEL,
                                             attrlist=['memberOf']))))

        # Re-enable non orphans
        map(lambda entry: ldap_conn.ldap_update(entry, [(_ldap.MOD_REPLACE, 'employeeType', None)]),
            map(lambda dn: dn[0],
                filter(lambda a: 'memberOf' in a[1].keys(),
                       ldap_conn.ldap_search(self._LDAP_TREE['accounts'],
                                             _ldap.SCOPE_ONELEVEL,
                                             attrlist=['memberOf'],
                                             filterstr='(employeeType=disabled)'))))

    def _getLDAPCompatibleProject(self, project, objectClass, ldap_conn):
        project = project.copy()
        project['objectClass'] = objectClass
        project['owner'] = [self._ldapCN(owner['employeeNumber'], ldap_conn) for owner in project.pop('owner', [])]
        project['member'] = [self._ldapCN(member['employeeNumber'], ldap_conn) for member in project.pop('member', [])]
        project['seeAlso'] = [self._ldapCN(seeAlso['employeeNumber'],
                                           ldap_conn) for seeAlso in project.pop('seeAlso', [])]
        project['uniqueMember'] = project['member']
        project.pop('tenants')
        project.pop('member' if objectClass is 'groupOfUniqueNames' else 'uniqueMember')

        return project

    def _getLDAPCompatibleAccount(self, account):
        account = account.copy()
        account['objectClass'] = 'inetOrgPerson'
        if extractOne('True', account.pop('isHidden'), score_cutoff=75):
            account['employeeType'] = 'hidden'

        return account

    # deprecated
    def _createRecord(self, project, ldap_conn):
        return filter(lambda r: len(r[1]), [
            ('objectClass', ['groupOfNames']),
            ('cn', [project['cn']]),
            ('o', project['o']),
            ('owner', map(lambda o: self._ldapCN(o['uid'], ldap_conn), project['owner'])),
            ('seeAlso', map(lambda a: self._ldapCN(a['uid'], ldap_conn), project['seeAlso'])),
            ('member', map(lambda m: self._ldapCN(m['uid'], ldap_conn), project['members'])),
            ('description', ['type:%s' % item for item in project['description']])
        ])

    # deprecated
    def _createTenantRecord(self, tenant, ldap_conn):
        record = self._createRecord(tenant, ldap_conn)
        record = map(lambda r: r if r[0] != 'objectClass' else (r[0], ['groupOfUniqueNames']), record)
        if len(record) == 7:
            record = map(lambda r: r if r[0] != 'owner' else ('uniqueMember', r[1]), record)
            record.pop(4)
            record.pop(4)
        else:
            record = map(lambda r: r if r[0] != 'member' else ('uniqueMember', r[1]), record)
        return record

    def _createOrUpdate(self, member_list, ldap_conn):
        new_records = filter(lambda m: not ldap_conn.ldap_search(self._LDAP_TREE['accounts'],
                                                                 _ldap.SCOPE_ONELEVEL,
                                                                 filterstr='employeeNumber=%s' % m['employeeNumber'],
                                                                 attrsonly=1),
                             member_list)

        map(lambda c: ldap_conn.ldap_add('cn=%s,%s' % (self._createCN(c, ldap_conn), self._LDAP_TREE['accounts']),
                                         _modlist.addModlist(self._getLDAPCompatibleAccount(c),
                                                             ignore_attr_types=['cn'])),
            new_records)

        map(lambda u: ldap_conn.ldap_update('%s' % self._ldapCN(u['employeeNumber'], ldap_conn),
                                            _modlist.modifyModlist(ldap_conn.ldap_search(self._LDAP_TREE['accounts'],
                                                                                         _ldap.SCOPE_ONELEVEL,
                                                                                         filterstr='employeeNumber=%s'
                                                                                         % u['employeeNumber'])[0][1],
                                                                   self._getLDAPCompatibleAccount(u),
                                                                   ignore_attr_types=['userPassword', 'cn'])),
            filter(lambda m: cmp(dict(self._getLDAPCompatibleAccount(m)),
                                 ldap_conn.ldap_search(self._LDAP_TREE['accounts'],
                                                       _ldap.SCOPE_ONELEVEL,
                                                       filterstr='employeeNumber=%s' % m['employeeNumber'],
                                                       attrlist=['displayName', 'objectClass', 'employeeType',
                                                                 'mobile', 'employeeNumber', 'sn',
                                                                 'mail', 'givenName'])[0][1]),
                   member_list))

        return new_records

    def _sendNewAccountEmails(self, new_accounts, project_type, ldap_conn):
        map(lambda d: map(lambda t: self.mailer.sendCannedMail(t,
                                                               self.mailer.CANNED_MESSAGES['new_devel_account'] if
                                                               project_type in [self.SDA, self.OS_TENANT] else
                                                               self.mailer.CANNED_MESSAGES['new_partner_account'],
                                                               d['cn'][0]),
                          d['mail']),
            map(lambda a: ldap_conn.ldap_search('ou=accounts,dc=forgeservicelab,dc=fi',
                                                _ldap.SCOPE_ONELEVEL,
                                                filterstr='employeeNumber=%s' % a['employeeNumber'],
                                                attrlist=['cn', 'mail'])[0][1],
                new_accounts))

    # deprecated
    def _ensureButlerService(self, record):
        if not any([member.startswith('cn=butler.service') for
                    member in filter(lambda attribute: attribute[0] == 'uniqueMember', record)[0][1]]):
            record = map(lambda r: r if r[0] != 'uniqueMember'
                         else ('uniqueMember',
                               ['cn=butler.service,ou=accounts,dc=forgeservicelab,dc=fi'] + r[1]), record)
        return record

    def _addAndNotify(self, dn, tenant, ldap_conn):
        add_butler = False
        if 'Digile.Platform' in dn:
            self.updater\
                .addUserToProject(ldap_conn.ldap_search('cn=butler.service,ou=accounts,dc=forgeservicelab,dc=fi',
                                                        _ldap.SCOPE_BASE,
                                                        attrlist=['employeeNumber'])[0][1]['employeeNumber'][0],
                                  tenant)

            add_butler = all([member['displayName'] != 'Butler Service' for member in tenant['member']])

        ldap_tenant = self._getLDAPCompatibleProject(tenant, 'groupOfUniqueNames', ldap_conn)
        if add_butler:
            ldap_tenant['uniqueMember'] += ['cn=butler.service,ou=accounts,dc=forgeservicelab,dc=fi']
        ldap_conn.ldap_add(dn, _modlist.addModlist(ldap_tenant))

        map(lambda ml: map(lambda e: self.mailer.sendCannedMail(e,
                                                                self.mailer.CANNED_MESSAGES['added_to_tenant'],
                                                                ldap_tenant['cn']),
                           ml),
            [ldap_conn.ldap_search(s, _ldap.SCOPE_BASE,
                                   attrlist=['mail'])[0][1]['mail'] for s in ldap_tenant['uniqueMember']])

    def _createTenants(self, tenant_list, project, ldap_conn):
        if tenant_list:
            map(lambda t: self._sendNewAccountEmails(self._createOrUpdate(t['member'], ldap_conn),
                                                     self.OS_TENANT, ldap_conn), tenant_list)
            map(lambda c: self._addAndNotify('cn=%s,cn=%s,%s' % (c['cn'], project['cn'], self._LDAP_TREE['projects']),
                                             c, ldap_conn),
                tenant_list)
        else:
            insightly_tenant = self.updater.createDefaultTenantFor(project)
            tenant = project.copy()
            tenant['o'] = str(insightly_tenant['PROJECT_ID'])
            tenant['uniqueMember'] = tenant.pop('owner', [])
            tenant.pop('seeAlso')
            self._sendNewAccountEmails(self._createOrUpdate(tenant['uniqueMember'], ldap_conn),
                                       self.OS_TENANT, ldap_conn)
            self._addAndNotify('cn=%(cn)s,cn=%(cn)s,%(sf)s' %
                               {'cn': project['cn'], 'sf': self._LDAP_TREE['projects']}, tenant, ldap_conn)

    def _create(self, project, project_type, ldap_conn):
        self._sendNewAccountEmails(self._createOrUpdate(project['member'], ldap_conn), project_type, ldap_conn)

        ldap_conn.ldap_add(
            'cn=%s,%s' % (project['cn'], self._LDAP_TREE['projects']),
            _modlist.addModlist(self._getLDAPCompatibleProject(project, 'groupOfNames', ldap_conn)))

        if project_type in [self.SDA, self.FPA_CRA]:
            self._createTenants(project['tenants'], project, ldap_conn)

        self.updater.updateProject(project, status=self.updater.STATUS_RUNNING)

        map(lambda a: map(lambda m: self.mailer.sendCannedMail(m, self.mailer.CANNED_MESSAGES['notify_admin_contact'],
                                                               a['displayName']),
                          a['mail']),
            project['seeAlso'])

        map(lambda a: map(lambda m: self.mailer.sendCannedMail(m, self.mailer.CANNED_MESSAGES['added_to_project'],
                                                               project['cn']), a['mail']), project['member'])

    def _updateAndNotify(self, dn, record, ldap_conn, is_tenant=False):
        ldap_record = ldap_conn.ldap_search(dn, _ldap.SCOPE_BASE)[0][1]
        dict_record = self._getLDAPCompatibleProject(record,
                                                     'groupOfUniqueNames' if is_tenant else 'groupOfNames',
                                                     ldap_conn)

        if cmp(dict_record, ldap_record):
            ldap_conn.ldap_update(dn, _modlist.modifyModlist(ldap_record, dict_record))
            new_users = filter(lambda m: m not in (ldap_record['uniqueMember'] if 'uniqueMember' in ldap_record.keys()
                                                   else ldap_record['member']),
                               (dict_record['uniqueMember'] if 'uniqueMember' in dict_record.keys()
                                   else dict_record['member']))
            gone_users = filter(lambda m: m not in (dict_record['uniqueMember'] if 'uniqueMember' in dict_record.keys()
                                                    else dict_record['member']),
                                (ldap_record['uniqueMember'] if 'uniqueMember' in ldap_record.keys()
                                 else ldap_record['member']))

            if any(member_attribute in dict_record.keys() for member_attribute in ['member', 'uniqueMember']):
                map(lambda email_list: map(lambda e: self.mailer
                                                         .sendCannedMail(e,
                                                                         self.mailer.CANNED_MESSAGES['added_to_tenant']
                                                                         if any(self.OS_TENANT in s for s in
                                                                                dict_record['description']) else
                                                                         self.mailer.CANNED_MESSAGES[
                                                                             'added_to_project'],
                                                                         dict_record['cn'][0]), email_list),
                    map(lambda s: ldap_conn.ldap_search(s, _ldap.SCOPE_BASE, attrlist=['mail'])[0][1]['mail'],
                        new_users))
                map(lambda email_list: map(lambda e: self.mailer
                                           .sendCannedMail(e,
                                                           self.mailer.CANNED_MESSAGES[
                                                               'deleted_from_tenant']
                                                           if any(self.OS_TENANT in s for s in
                                                                  dict_record['description']) else
                                                           self.mailer.CANNED_MESSAGES[
                                                               'deleted_from_project'],
                                                           dict_record['cn'][0]), email_list),
                    map(lambda s: ldap_conn.ldap_search(s, _ldap.SCOPE_BASE, attrlist=['mail'])[0][1]['mail'],
                        gone_users))

    def _updateTenants(self, tenant_list, project, ldap_conn):
        map(lambda t: self._sendNewAccountEmails(self._createOrUpdate(t['member'], ldap_conn),
                                                 self.OS_TENANT, ldap_conn), tenant_list)

        ldap_tenant_cns = [cn[1]['cn'][0] for cn in ldap_conn.ldap_search('cn=%s,%s' %
                                                                          (project['cn'],
                                                                           self._LDAP_TREE['projects']),
                                                                          _ldap.SCOPE_ONELEVEL, attrlist=['cn'])]

        new_tenants = filter(lambda t: t['cn'] not in ldap_tenant_cns, tenant_list)
        removed_tenant_cns = filter(lambda cn: cn not in [tenant['cn'] for tenant in tenant_list], ldap_tenant_cns)

        if new_tenants or not tenant_list:
            self._createTenants(new_tenants, project, ldap_conn)

        if removed_tenant_cns:
            map(lambda cn: ldap_conn.ldap_delete('cn=%s,cn=%s,%s' % (cn, project['cn'], self._LDAP_TREE['projects'])),
                removed_tenant_cns)

        map(lambda u: self._updateAndNotify('cn=%s,cn=%s,%s' % (u['cn'], project['cn'], self._LDAP_TREE['projects']),
                                            u, ldap_conn, is_tenant=True),
            filter(lambda nonews: nonews not in new_tenants,
                   filter(lambda t: ldap_conn.ldap_search('cn=%s,cn=%s,%s' %
                                                          (t['cn'], project['cn'],
                                                           self._LDAP_TREE['projects']),
                                                          _ldap.SCOPE_BASE), tenant_list)))

    def _update(self, project, project_type, ldap_conn):
        ldap_record = ldap_conn.ldap_search('cn=%s,%s' % (project['cn'], self._LDAP_TREE['projects']),
                                            _ldap.SCOPE_BASE)

        if ldap_record:
            self._sendNewAccountEmails(self._createOrUpdate(project['member'], ldap_conn), project_type, ldap_conn)
            self._updateAndNotify('cn=%s,%s' % (project['cn'], self._LDAP_TREE['projects']),
                                  project,
                                  #   map(lambda t: (_ldap.MOD_REPLACE, t[0], t[1]),
                                  #       self._createRecord(project, ldap_conn)),
                                  ldap_conn)
            if project_type in [self.SDA, self.FPA_CRA]:
                self._updateTenants(project['tenants'], project, ldap_conn)
        else:
            self._create(project, project_type, ldap_conn)

    def _deleteTenants(self, tenant_list, project, ldap_conn):
        former_members = []
        map(lambda tenant: members.extend(ldap_conn.ldap_search(tenant, _ldap.SCOPE_BASE,
                                                                attrlist=['uniqueMember'])[0][1]['uniqueMember']),
            tenant_list)

        map(lambda tenant: ldap_conn.ldap_delete(tenant), tenant_list)

    def _delete(self, project, project_type, ldap_conn):
        tenant_list = ldap_conn.ldap_search('cn=%s,' % project['cn'] + self._LDAP_TREE['projects'],
                                            _ldap.SCOPE_SUBORDINATE, attrlist=['o'])
        for tenant in tenant_list or []:
            tenant[1]['o'] = tenant[1]['o'][0]

        map(lambda tenant: ldap_conn.ldap_delete(tenant[0]), tenant_list or [])
        ldap_conn.ldap_delete('cn=%s,%s' % (project['cn'], self._LDAP_TREE['projects']))

        map(lambda tenant: self.updater.updateProject(tenant[1], updateStage=False,
                                                      status=self.updater.STATUS_COMPLETED), tenant_list or [])
        self.updater.updateProject(project, updateStage=False, status=self.updater.STATUS_COMPLETED)

    _actions = {
        ACTION_CREATE: _create,
        ACTION_DELETE: _delete,
        ACTION_UPDATE: _update
    }

    def Action(self, action, data_list, ldap_conn):
        """Perform a CRUD action against LDAP.

        Triggers the generation of LDAP payload and executes the requested action against the LDAP connection.

        Args:
            action (str): The action to perform, one of ACTION_CREATE, ACTION_DELETE or ACTION_UPDATE.
            data_list (List): A list of the elements to use as payload for the CRUD action against LDAP.
            ldap_conn (ForgeLDAP): An initialized LDAP connection to perform actions against.
        """
        map(lambda k: map(lambda p: self._actions[action](self, p, k, ldap_conn), data_list[k]), data_list.keys())
        self._pruneAccounts(ldap_conn)