#!/usr/bin/env python3

import sys
import traceback
from typing import Dict, List, Tuple
import ldap
from ldap.controls import SimplePagedResultsControl
from aldpro_core.utils.logger import Logger
from django.conf import settings

logger = Logger('aldpro-mp')

BASE_DIR = "/opt/rbta/ad/mgmtportal/api/core"

if BASE_DIR not in sys.path:
    sys.path.insert(0, BASE_DIR)


APPLDAP_IPA_SERVER = settings.LDAP_SERVER
APPLDAP_IPA_USERNAME = settings.DISCOVER_USERNAME
APPLDAP_IPA_PASSWORD = settings.DISCOVER_PASSWORD

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


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={APPLDAP_IPA_USERNAME},cn=sysaccounts,cn=etc,{BASE_DN}",
            APPLDAP_IPA_PASSWORD,
        )
    except ldap.LDAPError as e:
        ldap_disconnect(initialize_ldap)
        sys.exit(f"Ошибка соединения с ldap {e}")
    return initialize_ldap


def delete_ldap_entries(
    entries: List[Tuple[str, Dict[str, List[bytes]]]],
    domain_controller: str = APPLDAP_IPA_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:
        sys.exit(f"Ошибка при удалении объектов {e}")
    finally:
        ldap_disconnect(initialize_ldap)


def fetch_ldap_result(
    search_base: str,
    filter_str: str = '(objectClass=*)',
    domain_controller: str = APPLDAP_IPA_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:
        sys.exit(f"Ошибка поиска сущностей {e}")
    finally:
        ldap_disconnect(initialize_ldap)
    return result


def delete_aci(objects_to_delete, domain_controller: str = APPLDAP_IPA_SERVER,):
    for entry in objects_to_delete:
        aci, target = generate_aci_string(entry)
        try:
            logger.info("Выполняем удаление 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):
            print(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:
        sys.exit(
            "Очистка разрешений остановлена - "
            "есть роли в процессе сборки: {}".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)


if __name__ == "__main__":
    try:
        check_roles_processing()
        cleanup_objects("privileges")
        clean_up_aci("permissions")
        cleanup_objects("permissions")
        clear_perm_objects()
    except Exception:
        error_message = traceback.format_exc()
        logger.error(error_message)
