#!/usr/bin/python3
# SPDX-FileCopyrightText: 2005-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""Migration tool for `univentionObjectType`"""


import argparse
import sys
from collections.abc import Callable

import univention.uldap


def buildModuleIdentifyMapping() -> dict[str, Callable]:
    import univention.admin.modules
    univention.admin.modules.update()
    return {
        name: module.identify
        for (name, module) in univention.admin.modules.modules.items()
        if hasattr(module, 'identify')
    }


def parseOptions() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description='Set the attribute univentionObjectType for each directory object')
    parser.add_argument('--verbose', action='store_true', default=False, help='do not hide warnings for unmatched component objects')
    parser.add_argument('-v', '--verify', action='store_true', default=False, help='check objects with already set univentionObjectType')
    parser.add_argument('-b', '--base', default='', help='only modify objects at or below SEARCHBASE', metavar='SEARCHBASE')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('-n', '--no-action', dest='action', action='store_false', help='do not modify the directory, show what would have been done')
    group.add_argument('-a', '--action', dest='action', action='store_true', help='do modify the directory')

    options = parser.parse_args()
    return options


def warningSupressed(dn: str, attributes: dict[str, list[bytes]]) -> bool:
    # cn=admin,$ldap_base cn=backup,$ldap_base
    ocs = set(attributes['objectClass'])
    if set(attributes.keys()) == {'objectClass', 'userPassword', 'cn', 'sn'} and ocs == {b'top', b'person'}:
        if attributes['sn'] == attributes['cn'] and len(attributes['cn']) == 1 and attributes['cn'][0] in (b'admin', b'backup'):
            return True
    # Kerberos principal objects
    if ocs == {b'top', b'account', b'krb5Principal', b'krb5KDCEntry'}:
        return True
    if ocs == {b'top', b'person', b'krb5Principal', b'krb5KDCEntry'}:
        return True
    # samba idmap objects
    if ocs == {b'sambaIdmapEntry', b'sambaSidEntry'}:
        return True
    # old (2.4) Kolab objects
    if b'kolabSharedFolder' in ocs:
        return True
    if b'univentionKolabGroup' in ocs:
        return True
    # old (2.4) UMC ACLs
    if b'univentionConsoleACL' in ocs:
        return True
    if b'univentionConsoleOperations' in ocs:
        return True
    if b'univentionPolicyConsoleAccess' in ocs:
        return True
    # old (2.4) UDM visibility settings
    if b'univentionPolicyAdminSettings' in ocs:
        return True
    if b'univentionAdminUserSettings' in ocs:
        return True
    return warningHidden(dn, attributes)


def warningHidden(dn: str, attributes: dict[str, list[bytes]]) -> bool:
    ocs = set(attributes['objectClass'])
    if warningHidden.verbose:
        return False
    if b'univentionCitrixUserSessionsClass' in ocs:
        return True
    if b'univentionPolicyThinClientUser' in ocs:
        return True
    if b'univentionThinClientSession' in ocs:
        return True
    return b'univentionThinClientAutostart' in ocs


warningHidden.verbose = False  # type: ignore


def main(options: argparse.Namespace) -> bool:
    errorsOccurred = False
    searchFilter = '(objectClass=*)' if options.verify else '(!(objectClass=univentionObject))'
    moduleIdentify = buildModuleIdentifyMapping()
    uldap = univention.uldap.getAdminConnection()
    if options.action is not True:
        uldap.modify = lambda dn, changes: sys.stdout.write('Would modify %r\n' % (dn, ))
    warningHidden.verbose = options.verbose

    for (dn, attributes) in uldap.search(filter=searchFilter, base=options.base):
        matches = [
            module
            for (module, identify) in moduleIdentify.items()
            if identify(dn, attributes)
        ]
        if 'container/dc' in matches and ('container/ou' in matches or 'container/cn' in matches):
            # container/dc has priority (ldapbase ou=/cn= has multiple matches)
            matches = ['container/dc']

        if len(matches) == 1:
            if b'univentionObject' not in attributes['objectClass']:
                try:
                    changes = [
                        ('objectClass', attributes['objectClass'], attributes['objectClass'] + [b'univentionObject']),
                        ('univentionObjectType', [], [matches[0].encode('UTF-8')]),
                    ]
                    uldap.modify(dn, changes)
                except univention.uldap.ldap.INVALID_SYNTAX as e:
                    # this error should not happen, in case it does, it is an indicator that
                    # LDAP schema extensions are missing (Bug #26304)
                    print('ERROR: Could not set univentionObjectType! (%s)\nIt seems that the corresponding LDAP schema extensions are not installed correctly.' % e, file=sys.stderr)
                    return False
            elif attributes['univentionObjectType'][0].decode('utf-8') != matches[0]:
                errorsOccurred = True
                print('Mismatch for %r: univentionObjectType is %r but should be %r!' % (dn, attributes['univentionObjectType'][0].decode('utf-8'), matches), file=sys.stderr)
        elif len(matches) > 1:
            raise ValueError('Multiple matches for %r: %r!' % (dn, matches))
        else:
            if not warningSupressed(dn, attributes):
                print('Warning: No match for %r' % (dn, ), file=sys.stderr)
                if options.verbose:
                    for attr in ('objectClass', 'univentionObjectType'):
                        for value in attributes.get(attr, []):
                            print('\t%s: %s' % (attr, value))

    return not errorsOccurred


if __name__ == "__main__":
    options = parseOptions()
    if not main(options):
        sys.exit(1)
