#!/usr/bin/python3
#
# Univention Keycloak
#
# SPDX-FileCopyrightText: 2023-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
"""Check Migration Status to the Keycloak app"""

import subprocess
import sys
import time
from argparse import ArgumentParser

import ldif
from ldap.filter import filter_format

import univention.admin.uldap
from univention.config_registry import handler_set, ucr
from univention.udm import UDM
from univention.udm.exceptions import NoObject


GUIDE_URL = "<https://docs.software-univention.de/keycloak-migration/index.html>"


def create_ldif(objects, opt):
    print(f'\n\t  Backing up objects to {opt.backup_path}\n')
    with open(opt.backup_path, 'w+') as f:
        ldif_writer = ldif.LDIFWriter(f)
        for dn, obj in objects:
            ldif_writer.unparse(dn, obj)


def delete_objects(sso_obj):
    lo, _ = univention.admin.uldap.getAdminConnection()
    for dn, obj in sso_obj:
        try:
            lo.delete(dn)
            print(f'\t  Deleted {dn}')
        except univention.admin.uexceptions.base as e:
            print(f'\t  Failed to delete {dn} due: {e}', file=sys.stderr)


def keycloak_installed_in_domain():
    lo, _ = univention.admin.uldap.getMachineConnection()
    obj = lo.search(filter_format('(univentionService=%s)', ["keycloak"]))
    return obj


def get_sso_client_objects():
    lo, _ = univention.admin.uldap.getMachineConnection()
    oidc_obj = lo.search(
        filter='(univentionObjectType=oidc/rpservice)',
        base=lo.base,
    )
    saml_obj = lo.search(
        filter='(univentionObjectType=saml/serviceprovider)',
        base=lo.base,
    )
    return oidc_obj + saml_obj


def is_primary():
    return ucr.get('server/role') == 'domaincontroller_master'


def fix_uri_setting(opt, new_value):

    if not opt.create_sso_uri_setting:
        return False

    # create policy
    if is_primary() and ucr.is_true('ucs/server/sso/uri/autopolicy', True):
        mod = UDM.admin().version(2).get('policies/registry')
        policy_name = "sso_uri_domainwide_setting"
        ldap_base = ucr.get('ldap/base')
        position = f'cn=config-registry,cn=policies,{ldap_base}'
        mod = UDM.admin().version(2).get('policies/registry')
        try:
            # URL not found in UCR, try in LDAP first
            policy_obj = mod.get(f'cn={policy_name},{position}')
        except NoObject:
            if opt.force or input(f'Create UCR policy for ucs/server/sso/uri={new_value}: (y/N)').lower() == 'y':
                policy_obj = mod.new()
                policy_obj.position = position
                policy_obj.props.name = policy_name
                policy_obj.props.registry = {'ucs/server/sso/uri': new_value}
                policy_obj.save()
                base_obj = UDM.admin().version(2).get('container/dc').get(f'{ldap_base}')
                base_obj.policies += [policy_obj.dn]
                base_obj.save()

    # local setting
    if opt.force or input(f'Create local UCR variable for ucs/server/sso/uri={new_value}: (y/N)').lower() == 'y':
        handler_set([f'ucs/server/sso/uri={new_value}'])
        return True

    return False


def check_uri_setting():
    subprocess.call(['/usr/lib/univention-directory-policy/univention-policy-update-config-registry'])
    ucr.load()
    ucs_server_sso_uri = ucr.get('ucs/server/sso/uri')
    if not keycloak_installed_in_domain():
        return True
    return bool(ucs_server_sso_uri)


def delete_sso_objects(opt, sso_obj):
    if opt.delete:
        print(
            "\n\t  Do you really want to delete the objects?"
            "\n\t  SSO with SimpleSAMLphp or Kopano Connect will no longer work!"
            f"\n\t  In case you are unsure please read the migration guide: {GUIDE_URL}",
        )
        if opt.force or input('\nI want to remove all SSO objects: (y/N)').lower() == 'y':
            create_ldif(sso_obj, opt)
            delete_objects(sso_obj)
            return True
    return False


def migration_complete(opt):
    migrated = {"sso_objects": False, "uri_setting": False}

    sso_obj = None if not is_primary() else get_sso_client_objects()
    if not sso_obj:
        migrated["sso_objects"] = True

    uri_setting = check_uri_setting()
    if uri_setting:
        migrated["uri_setting"] = True

    print('Checking Keycloak migration status ...')
    if not all(migrated.values()):
        print(
            "\nStarting with UCS 5.2 the Keycloak app replaces SimpleSAMLphp"
            "\nand the Kopano Konnect app as the default identity provider in UCS."
            "\nBefore the update to 5.2 can start, this domain has to be migrated"
            "\nto Keycloak."
            "\n\nThis migration has not happend yet!"
            "\n\nThe following old SimpleSAMLphp/Kopano Konnect objects have been found:\n",
            file=sys.stderr,
        )
        print(
            "\nPlease read the UCS 5.2 release notes <https://docs.software-univention.de/release-notes/5.2-0/en/index.html>"
            f"\nand the Keycloak migration guide: {GUIDE_URL}"
            "\nfor how to migrate your domain to Keycloak.",
            file=sys.stderr,
        )

    if not migrated["sso_objects"]:
        print(
            "\n\t- The following old SimpleSAMLphp/Kopano Konnect objects have been found."
            "\n\t  They need to be removed before the update can happen:\n",
            file=sys.stderr,
        )
        for dn, obj in sso_obj:
            print(f"\t\t* {dn}", file=sys.stderr)
        # fix it
        if delete_sso_objects(opt, sso_obj):
            migrated["sso_objects"] = True
    elif is_primary():
        print("\n\t - Found no old udm sso objects")

    if not migrated["uri_setting"]:
        print(
            "\n\t - The UCR variable \"ucs/server/sso/uri\" is not set.\n"
            "\n\t   Starting with UCS 5.2 the Single-Sign On settings have been changed"
            "\n\t   Instead of keycloak/server/sso/fqdn and keycloak/server/sso/path"
            "\n\t   the new UCR variable ucs/server/sso/uri has to be set on all hosts in the domain"
            "\n\t   while keycloak/server/sso/fqdn and keycloak/server/sso/path will be only relevant"
            "\n\t   for the Keycloak app itself."
            "\n\t   Even though ucs/server/sso/uri is not used in UCS 5.0 yet,"
            "\n\t   please set ucs/server/sso/uri before upgrading to 5.2",
            file=sys.stderr,
        )
        kc_sso_fqdn = ucr.get('keycloak/server/sso/fqdn')
        kc_sso_path = ucr.get('keycloak/server/sso/path', '/')
        if kc_sso_fqdn:
            new_value = f'https://{kc_sso_fqdn}{kc_sso_path}'
            print(
                "\n\t   Since keycloak/server/sso/fqdn is set on the machine, you can"
                "\n\t   run this script using the --create-sso-uri-setting option"
                f"\n\t   to change this setting automatically to {new_value}."
                "\n\t   Additionally, if ucs/server/sso/uri/autopolicy is true, it will create"
                "\n\t   a UCR policy to distribute this setting in the domain."
                f"\n\n\t   In case you are unsure please read the migration guide: {GUIDE_URL}\n",
                file=sys.stderr,
            )

            # fix it
            if fix_uri_setting(opt, new_value):
                migrated["uri_setting"] = True
    else:
        print(f"\n\t - ucs/server/sso/uri ({ucr.get('ucs/server/sso/uri')}) exists or is not relevant")

    if all(migrated.values()):
        print('\nMigration to Keycloak complete.')
        sys.exit(0)
    else:
        print('\nMigration to Keycloak incomplete, update to UCS 5.2 not possible', file=sys.stderr)
        sys.exit(1)


def parse_arguments(parser):
    parser.add_argument('-d', '--delete', action='store_true', help='Delete all saml/serviceprovider, and oic/rpservice UDM objects. A backup file will be created')
    parser.add_argument('-f', '--force', action='store_true', help='Force deletion of all SAML or OIDC related UDM objects, Force setting new SSO URI setting')
    parser.add_argument('-c', '--create-sso-uri-setting', action='store_true', help='Create new sso uri setting if possible')
    parser.add_argument('--backup-path', default=f'/var/univention-backup/saml_oidc_{time.time()}.ldif', help='Path for backup files')
    return parser.parse_args()


if __name__ == '__main__':
    parser = ArgumentParser(description=__doc__)
    opt = parse_arguments(parser)
    migration_complete(opt)
