#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention UCS@school
# SPDX-FileCopyrightText: 2017-2026 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

"""Tool to delete users whose deletion grace time (ucsschoolPurgeTimestamp) has passed."""

import datetime
import logging
import pprint
import re
import sys
from argparse import ArgumentParser
from typing import TYPE_CHECKING, List  # noqa: F401

from ldap.filter import filter_format

from ucsschool.importer.factory import setup_factory
from ucsschool.importer.frontend.user_import_cmdline import UserImportCommandLine
from ucsschool.importer.utils.ldap_connection import get_admin_connection
from ucsschool.lib.models.utils import UniStreamHandler, ucr

if TYPE_CHECKING:
    from ucsschool.importer.models.import_user import ImportUser  # noqa: F401


def parse_cmdline():
    defaults = {"dry_run": False, "logfile": None, "verbose": False}
    parser = ArgumentParser(
        description="UCS@school delete tool for expired user accounts (ucsschoolPurgeTimestamp)"
    )
    parser.add_argument(
        "-c",
        "--conffile",
        help="Configuration file to use (see /usr/share/doc/ucs-school-import for an explanation on "
        "configuration file stacking).",
    )
    parser.add_argument("-l", "--logfile", help="Write to additional logfile (always verbose).")
    parser.add_argument(
        "-n",
        "--dry-run",
        dest="dry_run",
        action="store_true",
        help="Dry-run: don't actually commit changes to LDAP [default: %(default)s].",
    )
    parser.add_argument(
        "-q",
        "--quiet",
        action="store_true",
        help="Disable output on the console except errors [default: %(default)s].",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable debugging output on the console (overwrites setting from configuration files) "
        "[default: %(default)s].",
    )
    parser.set_defaults(**defaults)

    args = parser.parse_args()
    if args.quiet and args.verbose:
        parser.error('Options "quiet" and "verbose" are mutually exclusive.')
    return args


def roles_from_ocs(ocs, a_user, school):  # type: (List[str], ImportUser, str) -> List[str]
    class FakeUdmCls(object):
        """Fake UDM user object class for use with User.get_class_for_udm_obj()"""

        def __init__(self, ocs):
            self.oldattr = {"objectClass": [x.encode("UTF-8") for x in ocs]}

    kls = a_user.get_class_for_udm_obj(FakeUdmCls(ocs), school)
    return [] if kls is None else kls.roles


def purge_timestamp2date(purge_timestamp):
    return datetime.datetime.strptime(purge_timestamp, "%Y%m%d%H%M%SZ")


def shadow_expire2date(shadow_expire):
    return datetime.datetime.utcfromtimestamp(int(shadow_expire) * 3600 * 24).date()


def main():
    # this closely follows ucsschool.importer.frontend.cmdline.CommandLine.main()
    uic = UserImportCommandLine()
    uic.args = parse_cmdline()
    # overwrite 'verbose' setting from configuration files
    uic.args.settings = {
        "disabled_checks": ["test_00_required_config_keys"],
        "dry_run": uic.args.dry_run,
        "verbose": uic.args.verbose,
    }
    # set logging configured by cmdline
    uic.setup_logging(uic.args.verbose, uic.args.logfile)
    logger = uic.logger
    if uic.args.quiet:
        loglevel = logging.ERROR
    elif uic.args.verbose:
        loglevel = logging.DEBUG
    else:
        loglevel = logging.INFO
    logger.setLevel(loglevel)
    for handler in logger.handlers:
        if isinstance(handler, UniStreamHandler):
            handler.setLevel(loglevel)

    logger.info("------ UCS@school import tool starting ------")
    if uic.args.conffile:
        uic.configuration_files.append(uic.args.conffile)
    else:
        # prevent InitialisationError
        uic.args.settings["source_uid"] = "PurgeExpiredUsers"
    config = uic.setup_config()

    logger.info("------ UCS@school import tool configured ------")
    logger.debug("Configuration is:\n%s", pprint.pformat(config))
    factory = setup_factory(config["factory"])
    lo, po = get_admin_connection()

    # use factory.make_import_user() to get the user class from the import configuration
    # (ImportUser, CustomerImportUser ...)
    a_user = factory.make_import_user([])

    today_s = datetime.datetime.today().strftime("%Y%m%d%H%M%SZ")
    filter_s = filter_format("(&(objectClass=ucsschoolType)(ucsschoolPurgeTimestamp<=%s))", (today_s,))
    expired_accounts = lo.search(
        filter_s, attr=["ucsschoolPurgeTimestamp", "shadowExpire", "objectClass"]
    )

    logger.info("Found %d expired accounts.", len(expired_accounts))
    for dn, attr in expired_accounts:
        logger.debug(
            "dn=%r ucsschoolPurgeTimestamp=%r shadowExpire=%r",
            dn,
            purge_timestamp2date(attr["ucsschoolPurgeTimestamp"][0].decode("UTF-8")).strftime(
                "%Y-%m-%d"
            ),
            shadow_expire2date(attr["shadowExpire"][0].decode("ASCII")).strftime("%Y-%m-%d")
            if attr.get("shadowExpire")
            else None,
        )

    ou_pattern = re.compile(r".*,ou=(\w+),{}$".format(ucr["ldap/base"]))
    for dn, attr in expired_accounts:
        logger.info("%sDeleting %r...", "(dry-run) " if uic.args.dry_run else "", dn)

        # get correct class to prevent ucsschool.lib warning 'UDM object <dn> is not .., but actually ..'
        # try to get school name in case legacy user type detection is used
        m = ou_pattern.match(dn)
        if m:
            school = m.groups()[0]
        else:
            school = ""
        roles = roles_from_ocs([x.decode("UTF-8") for x in attr["objectClass"]], a_user, school)
        if not roles:
            logger.warning("Could not determine roles for %r. Deletion aborted.", dn)
            continue
        # use specific ImportUser class to execute correct hooks
        user_obj = factory.make_import_user(roles)
        user = user_obj.from_dn(dn, None, lo)
        user.remove(lo)

    logger.info("Finished.")


if __name__ == "__main__":
    sys.exit(main())
