#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2022-2023 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
"""Migrate the LDAP-users from the old (st) to the new (c) UDM mapping"""

import argparse
import pipes
import re
import sys
from subprocess import check_call
from typing import Any, List, Optional, Tuple, Union

from ldap import LDAPError

import univention.admin.uldap
from univention.config_registry import handler_set, ucr


COUNTRY_CODE_REGEX = re.compile(b'^[A-Z]{2}$')

if ucr.get('server/role') != 'domaincontroller_master':
    print('This script can only be executed on the Primary Directory Node.')
    sys.exit(1)

lo, po = univention.admin.uldap.getAdminConnection()


class Migration:
    ldap_exceptions: List[Tuple[str, Any, LDAPError]] = []  # [(dn, attemped change, error)]

    def __init__(self) -> None:
        parser = argparse.ArgumentParser(description='Transfer the LDAP attribute values of users from "st" to "c" and let UDM map "country" to "c" (instead of "st").')
        parser.add_argument(
            '--check', action='store_true',
            help="Makes a dry run by printing out all modifications which would be done. Exitcode 3 is returned if the migration hasn't happened yet and is possible.",
        )
        parser.add_argument('-v', '--verbose', action='count', help='increase verbosity.')
        self.args = parser.parse_args()
        if self.args.check and not self.args.verbose:
            self.args.verbose = 3

        if ucr.is_false('directory/manager/web/modules/users/user/map-country-to-st'):
            print('The migration seems to be done as the UCR variable "directory/manager/web/modules/users/user/map-country-to-st" is not true.')
            sys.exit(0)

        user_filter = '(&(|(univentionObjectType=users/user)(univentionObjectType=users/contact)(univentionObjectType=settings/usertemplate))(st=*))'
        self.ldap_users_with_st: List[Tuple[str, dict]] = lo.search(filter=user_filter)

        self.migrate_users()

        if self.ldap_exceptions:
            print('%d errors occurred.' % (len(self.ldap_exceptions),), file=sys.stderr)
            for dn, ml, exc in self.ldap_exceptions:
                print(f'{dn}: {exc} (modifications: {ml})', file=sys.stderr)
            sys.exit(1)

        self.create_ucr_policy()

        if self.args.check:
            sys.exit(3)  # migration possible exit code

    def create_ucr_policy(self):
        ldap_base = ucr.get('ldap/base')

        command = [
            'univention-directory-manager', 'policies/registry', 'create',
            '--ignore_exists',
            '--position', f'cn=config-registry,cn=policies,{ldap_base}',
            '--set', 'name=map-country-to-st',
            '--set', 'registry=directory/manager/web/modules/users/user/map-country-to-st false',
        ]
        if self.args.verbose:
            print('Creating UCR policy (via', ' '.join(pipes.quote(i) for i in command), ')')
        if not self.args.check:
            check_call(command)

        command = [
            'univention-directory-manager', 'container/dc', 'modify',
            '--dn', ldap_base,
            '--policy-reference', f'cn=map-country-to-st,cn=config-registry,cn=policies,{ldap_base}',
        ]
        if self.args.verbose:
            print('Referencing UCR policy to all computers (via', ' '.join(pipes.quote(i) for i in command), ')')
        if not self.args.check:
            check_call(command)

        if self.args.verbose:
            print('Set UCR variable to apply the new mapping (via ucr set directory/manager/web/modules/users/user/map-country-to-st=false)')
        if not self.args.check:
            handler_set(['directory/manager/web/modules/users/user/map-country-to-st=false'])

        command = [
            'univention-directory-manager', 'container/dc', 'modify',
            '--dn', ldap_base,
            '--policy-reference', f'cn=map-country-to-st,cn=config-registry,cn=policies,{ldap_base}',
        ]
        if self.args.verbose:
            print('Referencing UCR policy to all computers (via', ' '.join(pipes.quote(i) for i in command), ')')
        if not self.args.check:
            check_call(command)

    def ldap_modify(
        self,
        dn: str,
        changes: List[Tuple[str, Optional[Union[List[bytes], bytes]], Optional[Union[List[bytes], bytes]]]],
    ) -> None:
        """Write to LDAP while considering verbosity, dryrun, etc"""
        if self.args.verbose:
            def as_str(text) -> str:
                """convert text to str to prepare it for printing."""
                if not text:
                    return '<nothing>'
                if isinstance(text, (list, tuple)):
                    return ','.join(as_str(i) for i in text)
                if isinstance(text, bytes):
                    return f'"{text.decode("utf-8", "replace")}"'
                return f'"{text}"'

            print(f'Changes for LDAP object "{dn}":')
            print("\n".join([f' - attribute "{_a}": from {as_str([_o])} -> {as_str(_n)}' for _a, _o, _n in changes]))

        if not self.args.check:
            try:
                lo.modify(dn, changes, exceptions=True, ignore_license=True)
            except LDAPError as exc:
                self.ldap_exceptions.append((dn, changes, exc))

    def migrate_users(self) -> None:
        """Move the value of st to c in the LDAP user objects"""
        for dn, user in self.ldap_users_with_st:
            if not COUNTRY_CODE_REGEX.match(user['st'][0]):
                if self.args.verbose:
                    print(f"Warning: {dn} skipped, cannot migrate values since the st value does not seems to be a country-code. (st={user['st']}).")
            elif 'c' in user and (user['c'] != user['st']):
                if self.args.verbose:
                    print(f"Warning: {dn} skipped, cannot migrate values since c is already defined (st={user['st']}, c={user.get('c')}).")
            else:
                modifications = [
                    ('c', None, user['st']),
                    ('st', user['st'], None),
                ]
                if b'univentionPerson' not in user['objectClass']:
                    modifications.append(('objectClass', None, b'univentionPerson'))
                self.ldap_modify(dn, modifications)


if __name__ == '__main__':
    Migration()
