#!/usr/bin/env python3

import hashlib
import json
import os
import re
import sys
import time
import traceback
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Set, Tuple, Union

BASE_DIR = "/opt/rbta/ad/mgmtportal/api/core"
if BASE_DIR not in sys.path:
    sys.path.insert(0, BASE_DIR)

os.environ["DJANGO_SETTINGS_MODULE"] = "project.settings"

import ldap
from aldpro.ROCO.role.logic import RoleLogic
from aldpro_core.ldap_om.auth.vanilla_auth.auth import PasswordAuth
from aldpro_logging import SyslogLogger
from django.conf import settings
from ldap.controls import SimplePagedResultsControl

logger = SyslogLogger("aldpro-roles-management")


BASE_DN = settings.BASE_DN
LDAP_SERVER = settings.LDAP_SERVER
ROLESERVICE_USERNAME = settings.ROLESERVICE_USERNAME
ROLESERVICE_PASSWORD = settings.ROLESERVICE_PASSWORD

LDAP_SSL = settings.LDAP_SSL
LDAP_PROTO = "ldaps" if LDAP_SSL else "ldap"
LDAP_PORT = 636 if LDAP_SSL else 389
PAGE_CONTROL: SimplePagedResultsControl = SimplePagedResultsControl(True, size=150, cookie="")

MAIN_ADMINISTRATOR_PRIVILEGES = {
    "ALDPRO - AD Connections Administrators",
    "ALDPRO - Additional Automation Tasks Administrators",
    "ALDPRO - Automation Tasks Reader",
    "ALDPRO - Computers remote access",
    "ALDPRO - DHCP Servers Administrators",
    "ALDPRO - DNS Administrators",
    "ALDPRO - Delegation Administrators",
    "ALDPRO - Domain Controllers Administrators",
    "ALDPRO - Event Log Administrators",
    "ALDPRO - File Servers Administrators",
    "ALDPRO - Group Policies Administrator",
    "ALDPRO - Group Policy Additional Parameters Administrators",
    "ALDPRO - HBAC Administrator",
    "ALDPRO - Host Administrator",
    "ALDPRO - Host Group Administrator",
    "ALDPRO - IPA Configuration Administrators",
    "ALDPRO - Installer Server Computers Administrators",
    "ALDPRO - Installer Server Systems Administrators",
    "ALDPRO - Installer Servers Administrators",
    "ALDPRO - Kerberos Administrators",
    "ALDPRO - Manage Syncer",
    "ALDPRO - Manage Trusts",
    "ALDPRO - Manage Update Policies",
    "ALDPRO - Monitoring Administrators",
    "ALDPRO - NTP Servers Administrators",
    "ALDPRO - Organizational Units Administrator",
    "ALDPRO - Password Policy Administrators",
    "ALDPRO - Preserved User Manager",
    "ALDPRO - Print Servers Administrators",
    "ALDPRO - Profiles Administrators",
    "ALDPRO - Proxy Addresses Administrators",
    "ALDPRO - Proxy Addresses Reader",
    "ALDPRO - Replication Administrators",
    "ALDPRO - Repository Administrators",
    "ALDPRO - Repository Servers Administrators",
    "ALDPRO - Repository Versions Administrators",
    "ALDPRO - Service Administrators",
    "ALDPRO - Sites and services Administrators",
    "ALDPRO - Software Administrators",
    "ALDPRO - Software Catalogs Administrators",
    "ALDPRO - Software Configuration Templates Administrators",
    "ALDPRO - Software Packages Administrators",
    "ALDPRO - Software Parameters Administrators",
    "ALDPRO - Sudo Administrator",
    "ALDPRO - Sysaccounts Administrator",
    "ALDPRO - User Group Administrator",
    "ALDPRO - User Manager",
    "ALDPRO - User Reader",
    "Replication Administrators",
    "Users History - Read",
}


def generate_aci_string(permission: Dict[str, List[bytes]]) -> Tuple[str, str]:
    """__generate_aci_string Сгенерировать строку aci и получить целевое дерево для добавления aci из разрешения

    Args:
        permission (Dict[str, List[bytes]]): Сырые данные полученные из разрешения

    Returns:
        Tuple[str, str]: Кортеж с результируещем aci и целевым деревом
    """
    raw_permission = permission[1]
    result_aci = []
    perm_location = "".join(perm_location.decode() for perm_location in raw_permission.get("ipaPermLocation"))
    if (
        not raw_permission.get("ipaPermIncludedAttr")
        and not raw_permission.get("ipaPermTargetFilter")
        and not raw_permission.get("ipaPermTarget")
    ):
        return
    if raw_permission.get("ipaPermIncludedAttr"):
        result_aci.append(
            '(targetattr = "{}")'.format(
                " || ".join(target_attr.decode() for target_attr in raw_permission.get("ipaPermIncludedAttr"))
            )
        )
    if raw_permission.get("ipaPermTarget"):
        result_aci.append(
            '(target = "ldap:///{}")'.format("".join(target.decode() for target in raw_permission.get("ipaPermTarget")))
        )
    if raw_permission.get("ipaPermTargetFilter"):
        if len(raw_permission.get("ipaPermTargetFilter")) > 1:
            result_aci.append(
                '(targetfilter = "(&{})")'.format(
                    "".join(
                        target_filter.decode() for target_filter in sorted(raw_permission.get("ipaPermTargetFilter"))
                    )
                )
            )
        else:
            result_aci.append(
                '(targetfilter = "{}")'.format(
                    "".join(target_filter.decode() for target_filter in raw_permission.get("ipaPermTargetFilter"))
                )
            )
    if raw_permission.get("cn"):
        result_aci.append(
            '(version 3.0;acl "permission:{}";'.format(
                "".join(version.decode() for version in raw_permission.get("cn"))
            )
        )
    if raw_permission.get("ipaPermRight"):
        result_aci.append(
            "allow ({}) ".format(",".join(allow.decode() for allow in sorted(raw_permission.get("ipaPermRight"))))
        )
    result_aci.append(f'groupdn = "ldap:///{permission[0]}";)')
    return ("".join(result_aci), perm_location)


def get_cn(obj):
    return obj[1].get("cn", [b""])[-1].decode()


def ldap_disconnect(initialize_ldap: ldap.ldapobject.SimpleLDAPObject) -> None:
    """Закрыть соединение к ldap

    Args:
        initialize_ldap (ldap.ldapobject.SimpleLDAPObject): объект соединения
    """
    logger.info("Закрываем соединение с ldap")
    initialize_ldap.unbind()


def ldap_connect(domain_controller: str) -> ldap.ldapobject.SimpleLDAPObject:
    """Открыть соединенеие к ldap

    Args:
        domain_controller (str): Адрес контролера домена, к которому подсодениться

    Returns:
        ldap.ldapobject.SimpleLDAPObject: объект соединения
    """
    logger.info("Открываем соединение с ldap")
    ldap_url = f"{LDAP_PROTO}://{domain_controller}:{LDAP_PORT}"
    initialize_ldap = ldap.initialize(ldap_url)
    try:
        initialize_ldap.simple_bind_s(
            f"uid={ROLESERVICE_USERNAME},cn=sysaccounts,cn=etc,{BASE_DN}",
            ROLESERVICE_PASSWORD,
        )
    except ldap.LDAPError as e:
        ldap_disconnect(initialize_ldap)
        raise ConnectionError(f"Ошибка соединения с ldap {e}")
    return initialize_ldap


def delete_ldap_entries(
    entries: List[Tuple[str, Dict[str, List[bytes]]]],
    domain_controller: str = LDAP_SERVER,
) -> None:
    """Удалить объекты

    Args:
        entries (List[Tuple[str, Dict[str, List[bytes]]]]): Перечень объектов
        domain_controller (str):  Адрес контролера домена, к которому подсодениться
    """
    logger.info("Выполняем удаление объектов")
    initialize_ldap = ldap_connect(domain_controller)
    try:
        for entry in entries:
            entry_cn = get_cn(entry)
            logger.info(f"Выполняем удаление объекта {entry_cn}")
            initialize_ldap.delete_s(entry[0])
            logger.info(f"Объект {entry_cn} удалён")
    except ldap.LDAPError as e:
        raise Exception(f"Ошибка при удалении объектов {e}")
    finally:
        ldap_disconnect(initialize_ldap)


def fetch_ldap_result(
    search_base: str,
    filter_str: str = "(objectClass=*)",
    domain_controller: str = LDAP_SERVER,
    attrlist=None,
    scope=ldap.SCOPE_ONELEVEL,
) -> List[Tuple[str, Dict[str, List[bytes]]]]:
    """Выполняет поиск в контейнере

    Args:
        domain_controller (str): Адрес контролера домена, к которому подсодениться
        search_base (str): Контейнер для поиска
        filter_str (str): Фильтр для поиска, по умолчанию - (objectClass=*) - вернет все объекты в контейнере

    Returns:
        List[Tuple[str, Dict[str, List[bytes]]]]: Перечень объектов и их cn
    """
    logger.info("Выполняем поиск сущностей")
    initialize_ldap = ldap_connect(domain_controller)
    PAGE_CONTROL: SimplePagedResultsControl = SimplePagedResultsControl(True, size=150, cookie="")
    if attrlist is None:
        attrlist = [
            "cn",
            "ipaPermLocation",
            "ipaPermIncludedAttr",
            "ipaPermRight",
            "ipaPermTarget",
            "ipaPermTargetFilter",
        ]
    try:
        message_id = initialize_ldap.search_ext(
            base=f"{search_base},{BASE_DN}",
            scope=scope,
            filterstr=filter_str,
            attrlist=attrlist,
            serverctrls=[PAGE_CONTROL],
        )
        result = []
        pages = 0
        while True:
            pages += 1
            _, rdata, _, serverctrls = initialize_ldap.result3(message_id)
            result.extend(rdata)
            controls = [
                control for control in serverctrls if control.controlType == SimplePagedResultsControl.controlType
            ]
            if not controls:
                print("The server ignores RFC 2696 control")
                break
            if not controls[0].cookie:
                break
            PAGE_CONTROL.cookie = controls[0].cookie
            message_id = initialize_ldap.search_ext(
                base=f"{search_base},{BASE_DN}",
                scope=scope,
                filterstr=filter_str,
                attrlist=attrlist,
                serverctrls=[PAGE_CONTROL],
            )
    except ldap.LDAPError as e:
        raise Exception(f"Ошибка поиска сущностей {e}")
    finally:
        ldap_disconnect(initialize_ldap)
    return result


def delete_aci(
    objects_to_delete,
    domain_controller: str = LDAP_SERVER,
):
    for entry in objects_to_delete:
        aci, target = generate_aci_string(entry)
        try:
            logger.info(f"Выполняем удаление aci: {aci}")
            initialize_ldap = ldap_connect(domain_controller)
            initialize_ldap.modify_s(target, [(1, "aci", bytes(aci, encoding="UTF-8"))])
        except (ldap.TYPE_OR_VALUE_EXISTS, ldap.NO_SUCH_ATTRIBUTE):
            logger.error(f"ACI {aci} не найдено цель: {target}")
            continue
        finally:
            ldap_disconnect(initialize_ldap)


def clean_up_aci(target):
    """Очищает раздел от неиспользуемых aci

    Args:
        target ('permissions'): Устанавливает цель: разрешения
    """
    objects_to_delete = fetch_ldap_result(f"cn={target},cn=pbac", "(&(aldproMetaPermissionsContext=*)(!(member=*)))")
    if objects_to_delete:
        delete_aci(objects_to_delete)


def cleanup_objects(
    target: str,
) -> None:
    """Очищает раздел от неиспользуемых сущностей

    Args:
        target ('privileges' | 'permissions'): Устанавливает цель: привилегии или разрешения
    """
    filter_str = {
        "privileges": "(&(aldproMetaPrivilegesContext=*)(!(member=*)))",
        "permissions": "(&(aldproMetaPermissionsContext=*)(!(member=*)))",
    }
    objects_to_delete = fetch_ldap_result(f"cn={target},cn=pbac", filter_str[target])
    if objects_to_delete:
        delete_ldap_entries(objects_to_delete)


def check_roles_processing() -> None:
    roles_processing = fetch_ldap_result("cn=roles,cn=accounts", "(aldproMetaRoleRenderStatus=processing)")
    if roles_processing:
        raise Exception(
            "Очистка разрешений остановлена - "
            "есть роли в процессе сборки: {}".format(", ".join([get_cn(_) for _ in roles_processing]))
        )


def clear_perm_objects() -> None:
    """Удаляет clear_perm_objects у которых нет связи с permissions."""

    perm_obj = fetch_ldap_result(
        search_base="cn=perm_objects,cn=etc",
        filter_str="(objectClass=aldproMetaPermObject)",
        attrlist=["cn", "aci"],
        scope=ldap.SCOPE_SUBTREE,
    )

    permissions = fetch_ldap_result(
        search_base="cn=permissions,cn=pbac",
        filter_str="(cn=ALDPRO ROCO*)",
    )

    names_permission = []
    for dn, data in permissions:
        names_permission.append(data.get("cn")[0].decode())

    perm_objects_for_delete = []
    for dn, data in perm_obj:
        if data.get("aci"):
            for aci in data.get("aci"):
                aci = aci.decode()
                permission_from_aci = aci.split("permission:")[1].split('"')[0]
                if permission_from_aci not in names_permission:
                    perm_objects_for_delete.append((dn, {"cn": data.get("cn")}))
                    break

    if perm_objects_for_delete:
        delete_ldap_entries(perm_objects_for_delete)


class CheckInRestore:
    def __ldap_disconnect(self, initialize_ldap: ldap.ldapobject.SimpleLDAPObject) -> None:
        """Закрыть соединение к ldap

        Args:
            initialize_ldap (ldap.ldapobject.SimpleLDAPObject): объект соединения
        """
        initialize_ldap.unbind()

    def __ldap_connect(self, domain_controller: str) -> ldap.ldapobject.SimpleLDAPObject:
        """Открыть соединенеие к ldap

        Args:
            domain_controller (str): Адрес контролера домена, к которому подсодениться

        Returns:
            ldap.ldapobject.SimpleLDAPObject: объект соединения
        """
        ldap_url = f"{LDAP_PROTO}://{domain_controller}:{LDAP_PORT}"
        ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
        initialize_ldap = ldap.initialize(ldap_url)
        initialize_ldap.set_option(ldap.OPT_REFERRALS, 0)
        initialize_ldap.protocol_version = ldap.VERSION3
        try:
            initialize_ldap.simple_bind_s(
                f"uid={ROLESERVICE_USERNAME},cn=sysaccounts,cn=etc,{BASE_DN}",
                ROLESERVICE_PASSWORD,
            )
        except ldap.LDAPError as e:
            self.__ldap_disconnect(initialize_ldap)
            raise ConnectionError(f"Ошибка соединения с ldap {e}")

        return initialize_ldap

    def __fetch_ldap_result(
        self,
        search_base: str,
        filter_str: str = "(objectClass=*)",
        domain_controller: str = LDAP_SERVER,
        attrlist=None,
        scope=ldap.SCOPE_ONELEVEL,
    ) -> List[Tuple[str, Dict[str, List[bytes]]]]:
        """Выполняет поиск в контейнере

        Args:
            domain_controller (str): Адрес контролера домена, к которому подсодениться
            search_base (str): Контейнер для поиска
            filter_str (str): Фильтр для поиска, по умолчанию - (objectClass=*) - вернет все объекты в контейнере

        Returns:
            List[Tuple[str, Dict[str, List[bytes]]]]: Перечень объектов и их cn
        """
        initialize_ldap = self.__ldap_connect(domain_controller)
        PAGE_CONTROL: SimplePagedResultsControl = SimplePagedResultsControl(True, size=150, cookie="")
        if attrlist is None:
            attrlist = [
                "cn",
            ]
        try:
            message_id = initialize_ldap.search_ext(
                base=f"{search_base},{BASE_DN}",
                scope=scope,
                filterstr=filter_str,
                attrlist=attrlist,
                serverctrls=[PAGE_CONTROL],
            )
            result = []
            pages = 0
            while True:
                pages += 1
                _, rdata, _, serverctrls = initialize_ldap.result3(message_id)
                result.extend(rdata)
                controls = [
                    control for control in serverctrls if control.controlType == SimplePagedResultsControl.controlType
                ]
                if not controls:
                    print("The server ignores RFC 2696 control")
                    break
                if not controls[0].cookie:
                    break
                PAGE_CONTROL.cookie = controls[0].cookie
                message_id = initialize_ldap.search_ext(
                    base=f"{search_base},{BASE_DN}",
                    scope=scope,
                    filterstr=filter_str,
                    attrlist=attrlist,
                    serverctrls=[PAGE_CONTROL],
                )
        except ldap.LDAPError as e:
            raise Exception(f"Ошибка поиска сущностей {e}")
        finally:
            self.__ldap_disconnect(initialize_ldap)
        return result

    def run(self) -> None:
        roles = self.__fetch_ldap_result(
            search_base="cn=roles,cn=accounts",
            filter_str="(&(objectClass=aldproMetaRole)(aldproMetaRoleRenderStatus=done))",
            attrlist=["aldproMetaRoleConfiguration", "memberOf"],
            scope=ldap.SCOPE_SUBTREE,
        )

        role_main_admin = self.__fetch_ldap_result(
            search_base="cn=roles,cn=accounts",
            filter_str="(cn=ALDPRO - Main Administrator)",
            attrlist=["aldproMetaRoleConfiguration", "memberOf"],
            scope=ldap.SCOPE_SUBTREE,
        )

        role_main_admin_dn, data = role_main_admin[0]
        has_all_privileges, missing_privileges = self.__is_role_valid(data=data, attrs=MAIN_ADMINISTRATOR_PRIVILEGES)
        if not has_all_privileges:
            for privilege in missing_privileges:
                privilege_dn = f"cn={privilege},cn=privileges,cn=pbac,{settings.BASE_DN}"
                self.__add_attrs(dn=privilege_dn, field="member", attribute=role_main_admin_dn)
                logger.info(f"В привилегию {privilege_dn} добавлена роль {role_main_admin_dn}")

        for role_dn, data in roles:
            has_all_privileges, missing_privileges = self.__is_role_valid(data=data)
            if not has_all_privileges:
                for privilege in missing_privileges:
                    privilege_dn = f"cn={privilege},cn=privileges,cn=pbac,{settings.BASE_DN}"
                    self.__add_attrs(dn=privilege_dn, field="member", attribute=role_dn)
                    logger.info(f"В привилегию {privilege_dn} добавлена роль {role_dn}")

    def __is_role_valid(
        self, data: Dict[str, Any], attrs: Optional[Set[str]] = None
    ) -> Tuple[bool, Set[Union[str, Any]]]:
        """Проверяет наличие в memberof необходимого набора привилегий."""

        aldpro_meta_role_configuration = data.get("aldproMetaRoleConfiguration")[0].decode("utf-8")
        aldpro_meta_role_configuration = json.loads(aldpro_meta_role_configuration)
        members_of = data.get("memberOf")

        privileges_in_config = set()
        if attrs:
            privileges_in_config.update(attrs)

        for key, value in aldpro_meta_role_configuration.items():
            if self.__is_privilege_hash_required(key):
                for data in value:
                    ou = data.get("ou")
                    ou_nested = data.get("ou_nested")
                    site = data.get("site")
                    context = self.__generate_context(ou=ou, ou_nested=ou_nested, site=site)
                    new_cn = self.__generate_new_cn(key, context)
                    privileges_in_config.add(new_cn)
            else:
                privileges_in_config.add(key)

        privileges_in_memberof = set()
        if members_of:
            for memberof in members_of:
                memberof = memberof.decode("utf-8")
                if "privileges" in memberof:
                    privilege_cn = memberof.split(",")[0].replace("cn=", "")
                    privileges_in_memberof.add(privilege_cn)

        has_all_privileges = privileges_in_config <= privileges_in_memberof
        missing_privileges = privileges_in_config - privileges_in_memberof

        return has_all_privileges, missing_privileges

    def __is_privilege_hash_required(self, privileges_name: str) -> bool:
        privilege = self.__fetch_ldap_result(
            filter_str=f"(cn={privileges_name})",
            attrlist=["aldproMetaPrivilegesIsOUPattern", "aldproMetaPrivilegesIsSitePattern"],
            search_base="cn=privileges,cn=pbac",
            scope=ldap.SCOPE_SUBTREE,
        )

        privilege_dn, data = privilege[0]

        is_ou_pattern = data.get("aldproMetaPrivilegesIsOUPattern")[0].decode("utf-8")
        is_site_pattern = data.get("aldproMetaPrivilegesIsSitePattern")[0].decode("utf-8")

        is_privilege_hash_required = False
        if is_ou_pattern == "TRUE" or is_site_pattern == "TRUE":
            is_privilege_hash_required = True

        return is_privilege_hash_required

    def __generate_context(self, ou: str, ou_nested: Optional[bool], site: str) -> str:
        """Генерирует контекст."""

        result_context_list = []
        if ou:
            result_context_list.append(ou)

        if ou_nested:
            result_context_list.append("NESTED")

        if site:
            result_context_list.append(site)

        result_context = "|".join(result_context_list)
        return result_context

    def __generate_new_cn(self, cn: str, context: str) -> str:
        """Генерирует cn с хэшем на основе контекста."""

        hash_ = hashlib.md5(context.encode()).hexdigest()
        new_cn = f"{cn} {hash_}"
        return new_cn

    def __add_attrs(self, dn: str, field: str, attribute: str) -> None:
        """Добавляет атрибут."""

        initialize_ldap = self.__ldap_connect(LDAP_SERVER)
        initialize_ldap.modify_s(dn, [(ldap.MOD_ADD, field, bytes(attribute, encoding="UTF-8"))])

    def __del_attrs(self, dn: str, field: str, attribute: str) -> None:
        """Удаляет атрибут."""

        initialize_ldap = self.__ldap_connect(LDAP_SERVER)
        initialize_ldap.modify_s(dn, [(ldap.MOD_DELETE, field, bytes(attribute, encoding="UTF-8"))])

    def check_invalid_uniqueid(self) -> None:
        """Проверяет наличие у пермишена в атрибуте member nsuniqueid=... и удаляет его."""
        permissions = self.__fetch_ldap_result(
            filter_str=f"(member=*+nsuniqueid=*)",
            attrlist=["member"],
            search_base="cn=permissions,cn=pbac",
            scope=ldap.SCOPE_SUBTREE,
        )

        pattern = r"\+nsuniqueid=[^,]+"

        for permission in permissions:
            permission_dn, data = permission

            members = data.get("member")

            for member in members:
                member = member.decode("utf-8")
                if "+nsuniqueid=" in member:
                    cleaned_member = re.sub(pattern, "", member)
                    try:
                        self.__add_attrs(dn=permission_dn, field="member", attribute=cleaned_member)
                    except ldap.TYPE_OR_VALUE_EXISTS:
                        logger.warning(
                            f"У разрешения {permission_dn} уже существует атрибут member со значением {cleaned_member}"
                        )
                    self.__del_attrs(dn=permission_dn, field="member", attribute=member)


def convert_time_value(value: str) -> int:
    time_map = {
        "s": 1,
        "m": 60,
        "h": 60 * 60,
    }
    digit = ""
    for i in value:
        if i.isdigit():
            digit += i
        else:
            digit = int(digit)
            digit *= time_map.get(i)
    return digit


def start_roles_render():
    logger.info("Запущен рендер ролей")
    try:
        pass_auth = PasswordAuth(
            server=LDAP_SERVER,
            base_dn=BASE_DN,
            ldap_login=f"uid={ROLESERVICE_USERNAME},cn=sysaccounts,cn=etc,{BASE_DN}",
            ldap_password=ROLESERVICE_PASSWORD,
        )
        role = RoleLogic(auth_class=pass_auth)
        role.process_pending()
    except Exception as e:
        logger.error(f"Рендер завершился с ошибкой {e}")
        logger.error(f"\n{traceback.format_exc()}")
    else:
        logger.info("Рендер успешно завершен")


def start_fixing():
    logger.info("Запущена проверка на uniqueid в member")
    try:
        check_in_restore = CheckInRestore()
        check_in_restore.run()
        check_in_restore.check_invalid_uniqueid()
    except Exception as e:
        logger.error(f"Проверка завершилась с ошибкой: {e}")
        logger.error(f"\n{traceback.format_exc()}")
    else:
        logger.info("Проверка успешно завершена")


def start_grabage_collector():
    logger.info("Запущена очистка мусора")
    try:
        check_roles_processing()
        cleanup_objects("privileges")
        clean_up_aci("permissions")
        cleanup_objects("permissions")
        clear_perm_objects()
    except Exception as e:
        logger.error(f"Очистка мусора завершена с ошибкой: {e}")
        logger.error(f"\n{traceback.format_exc()}")
    else:
        logger.info("Очистка мусора успешно завершена")


if __name__ == "__main__":
    desc = """
        Демон обработки ролей ALD Pro.
        Данный юнит выполняет следующие действия:
        \t- Периодический рендер ролей
        \t- Периодическое исправление ошибок связанных с составом ролей из-за конфликтов репликации
        \t- Очиста ТОЛЬКО СГЕНЕРИРОВАННЫХ механизмом рендера привилегий/разрешений/aci, которые не используются в указанный период времени
        """
    parser = ArgumentParser(description=desc, formatter_class=RawDescriptionHelpFormatter)
    parser.add_argument(
        "-r",
        "--render",
        default="1m",
        metavar="Int[s|m|h]",
        help="Устанавливает период в который будет запускаться рендер ролей ожидающих активацию (default: 1m). Значение 0 отключит данную функцию.",
    )
    parser.add_argument(
        "-f",
        "--fixer",
        default="1h",
        metavar="Int[s|m|h]",
        help="Устанавливает период в который будет запускаться проверка/исправление целостности ролей после репликации (default: 1h). Значение 0 отключит данную функцию.",
    )
    parser.add_argument(
        "-u",
        "--utilizer",
        default="24h",
        metavar="Int[s|m|h]",
        help="Устанавливает период в который будет запускаться сборщик мусора (default: 24h). Значение 0 отключит данную функцию.",
    )
    parser.add_argument(
        "-ust",
        "--utilizer-start-time",
        default="00:00",
        metavar="HH:MM",
        help="Устанавливает примерное время в которое должен будет запускаться сборщик мусора +- минимальное значение среди остальных флагов (default: 00:00)",
    )

    args = parser.parse_args()

    render_period = convert_time_value(args.render.lower())
    fixing_period = convert_time_value(args.fixer.lower())
    utilization_period = convert_time_value(args.utilizer.lower())
    utilization_start_time = datetime.strptime(args.utilizer_start_time, "%H:%M")

    current_time = datetime.now()
    fixing_last_run = current_time - timedelta(hours=1)
    fixing_run_before = False
    utilization_last_run = fixing_last_run
    utilization_run_before = False

    if min(render_period, fixing_period, utilization_period) != render_period:
        raise ValueError("Рендер ролей должен запускаться чаще остальных функций демона")

    while True:
        if render_period != 0:
            start_roles_render()

        check_fixing_period = (current_time - fixing_last_run).seconds >= fixing_period
        if fixing_period != 0 and check_fixing_period:
            start_fixing()
            fixing_run_before = True
            fixing_last_run = current_time

        if utilization_period != 0 and fixing_run_before:
            if current_time >= utilization_start_time and not utilization_run_before:
                start_grabage_collector()
                utilization_run_before = True
                utilization_last_run = current_time
            fixing_run_before = False

        time.sleep(render_period)
        current_time = datetime.now()

        if (current_time - utilization_last_run).seconds >= utilization_period:
            utilization_run_before = False
