#!/usr/bin/python
from typing import Callable

from django.apps import apps
from django.conf import settings
from django.db.models import F
from django.utils.translation import gettext_lazy as _, gettext, get_language
from treelib import Tree
from treelib.exceptions import NodeIDAbsentError

from common.tree import TreeNode
from .models import Permission, ContentType

# 根节点
root_node_data = {
    'id': '$ROOT$',
    'name': _('All permissions'),
    'title': _('All permissions'),
    'pId': '',
}

# 第二层 view 节点，手动创建的
view_nodes_data = [
    {'id': 'view_console', 'name': _('Console view')},
    {'id': 'view_pam', 'name': _('Pam view')},
    {'id': 'view_workbench', 'name': _('Workbench view')},
    {'id': 'view_audit', 'name': _('Audit view')},
    {'id': 'view_setting', 'name': _('System setting')},
    {'id': 'view_other', 'name': _('Other')},
]

# 第三层 app 节点，手动创建
app_nodes_data = [
    {'id': 'users', 'view': 'view_console'},
    {'id': 'assets', 'view': 'view_console'},
    {'id': 'accounts', 'name': _('Accounts'), 'view': 'view_console'},
    {'id': 'perms', 'view': 'view_console'},
    {'id': 'terminal', 'name': _('Session audits'), 'view': 'view_audit'},
    {'id': 'audits', 'view': 'view_audit'},
    {'id': 'rbac', 'view': 'view_console'},
    {'id': 'settings', 'view': 'view_setting'},
    {'id': 'tickets', 'view': 'view_other'},
    {'id': 'labels', 'view': 'view_console'},
    {'id': 'authentication', 'view': 'view_other'},
    {'id': 'ops', 'view': 'view_workbench'},
]

# 额外其他节点，可以在不同的层次，需要指定父节点，可以将一些 model 归类到这个节点下面
extra_nodes_data = [
    {"id": "cloud_import", "name": _("Cloud import"), "pId": "assets"},
    {"id": "account_node", "name": _("Accounts"), "pId": "view_pam"},
    {"id": "account_risk_node", "name": _("Account risk"), "pId": "view_pam"},
    {"id": "backup_account_node", "name": _("Backup account"), "pId": "view_pam"},
    {"id": "gather_account_node", "name": _("Gather account"), "pId": "view_pam"},
    {"id": "push_account_node", "name": _("Push account"), "pId": "view_pam"},
    {"id": "account_change_plan_node", "name": _("Account change secret"), "pId": "view_pam"},
    {'id': "my_assets", "name": _("My assets"), "pId": "view_workbench"},
    {'id': "operation_center", "name": _('App ops'), "pId": "view_workbench"},
    {'id': "notifications", "name": _("Notifications"), "pId": "view_setting"},
    {'id': "features", "name": _("Feature"), "pId": "view_setting"},
    {'id': "authentication_setting", "name": _("Authentication"), "pId": "view_setting"},
    {'id': "storage", "name": _("Storage"), "pId": "view_setting"},
    {'id': "components", "name": _("Component"), "pId": "view_setting"},
    {'id': "remote_app", "name": _("Applet"), "pId": "view_setting"},
    {'id': "security", "name": _("Security"), "pId": "view_setting"},
    {'id': "appearance", "name": _("Appearance"), "pId": "view_setting"},
    {'id': "tasks", "name": _("Task"), "pId": "view_setting"},
    {'id': "license", "name": _("License"), "pId": "view_setting"},
    {'id': "other", "name": _("Other"), "pId": "view_setting"},
    {'id': "job_audit", "name": _("Job audit"), "pId": "view_audit"},
    {'id': "report_node", "name": _("Report"), "pId": "view_audit"},
]

# 将 model 放到其它节点下，而不是本来的 app 中
special_pid_mapper = {
    'common.permission': 'view_other',
    'acls.commandfilteracl': 'perms',
    'acls.commandgroup': 'perms',
    'acls.loginacl': 'perms',
    'acls.loginassetacl': 'perms',
    'acls.connectmethodacl': 'perms',
    'acls.datamaskingrule': 'perms',
    'xpack.account': 'cloud_import',
    'xpack.syncinstancedetail': 'cloud_import',
    'xpack.syncinstancetask': 'cloud_import',
    'xpack.syncinstancetaskexecution': 'cloud_import',
    'xpack.strategy': 'cloud_import',
    'xpack.strategyaction': 'cloud_import',
    'xpack.strategyrule': 'cloud_import',
    'accounts.account': 'account_node',
    'accounts.accounttemplate': 'account_node',
    'accounts.accountrisk': 'account_risk_node',
    'accounts.checkaccountengine': 'account_risk_node',
    'accounts.checkaccountautomation': 'account_risk_node',
    'accounts.checkaccountexecution': 'account_risk_node',
    'accounts.view_accountsession': 'view_pam',
    'accounts.view_accountactivity': 'view_pam',
    'accounts.integrationapplication': 'view_pam',
    'accounts.virtualaccount': 'view_pam',
    'accounts.backupaccountautomation': 'backup_account_node',
    'accounts.view_backupaccountexecution': 'backup_account_node',
    'accounts.add_backupaccountexecution': 'backup_account_node',
    "accounts.pushaccountautomation": "push_account_node",
    "accounts.view_pushaccountexecution": "push_account_node",
    "accounts.add_pushaccountexecution": "push_account_node",
    'accounts.view_pushsecretrecord': 'push_account_node',
    "accounts.gatheredaccount": "gather_account_node",
    "accounts.gatheraccountsautomation": "gather_account_node",
    "accounts.view_gatheraccountsexecution": "gather_account_node",
    "accounts.add_gatheraccountsexecution": "gather_account_node",
    "accounts.changesecretautomation": "account_change_plan_node",
    "accounts.view_changesecretexecution": "account_change_plan_node",
    "accounts.add_changesecretexecution": "account_change_plan_node",
    "accounts.view_changesecretrecord": "account_change_plan_node",
    'audits.ftplog': 'terminal',
    'perms.view_myassets': 'my_assets',
    'ops.job': 'operation_center',
    'ops.adhoc': 'operation_center',
    'ops.playbook': 'operation_center',
    'ops.jobexecution': 'operation_center',
    "rbac.view_console": "view_console",
    "rbac.view_audit": "view_audit",
    "report_node": "view_audit",
    "rbac.view_pam": "view_pam",
    'audits.usersession': 'view_audit',
    "rbac.view_workbench": "view_workbench",
    "rbac.view_webterminal": "view_workbench",
    "rbac.view_filemanager": "view_workbench",
    "rbac.view_systemtools": "view_workbench",
    'tickets.view_ticket': 'tickets',
    "audits.joblog": "job_audit",
    'oauth2_provider.accesstoken': 'authentication',
}


if settings.JDMC_ENABLED:
    view_nodes_data.append({'id': 'view_jdmc', 'name': _('JDMC console')})
    special_pid_mapper["rbac.view_jdmc"] = "view_jdmc"


special_setting_pid_mapper = {
    "rbac.view_userloginreport": "report_node",
    "rbac.add_userloginreport": "report_node",
    "rbac.delete_userloginreport": "report_node",
    "rbac.view_userchangepasswordreport": "report_node",
    "rbac.add_userchangepasswordreport": "report_node",
    "rbac.delete_userchangepasswordreport": "report_node",
    "rbac.view_assetstatisticsreport": "report_node",
    "rbac.add_assetstatisticsreport": "report_node",
    "rbac.delete_assetstatisticsreport": "report_node",
    "rbac.view_assetactivityreport": "report_node",
    "rbac.add_assetactivityreport": "report_node",
    "rbac.delete_assetactivityreport": "report_node",
    "rbac.view_accountstatisticsreport": "report_node",
    "rbac.add_accountstatisticsreport": "report_node",
    "rbac.delete_accountstatisticsreport": "report_node",
    "rbac.view_accountautomationreport": "report_node",
    "rbac.add_accountautomationreport": "report_node",
    "rbac.delete_accountautomationreport": "report_node",
    "settings.change_email": "notifications",
    "settings.change_sms": "notifications",
    "settings.change_systemmsgsubscription": "notifications",
    "settings.change_announcement": "features",
    "settings.change_ticket": "features",
    "settings.change_ops": "features",
    "settings.change_vault": "features",
    "settings.change_chatai": "features",
    "settings.view_chatprompt": "features",
    "settings.change_virtualapp": "features",
    "settings.change_auth": "authentication_setting",
    "terminal.replaystorage": "storage",
    "terminal.commandstorage": "storage",
    'terminal.applet': 'remote_app',
    'terminal.applethost': 'remote_app',
    'terminal.appletpublication': 'remote_app',
    'terminal.applethostdeployment': 'remote_app',
    "terminal.virtualapp": "remote_app",
    "terminal.virtualapppublication": "remote_app",
    "terminal.appprovider": "remote_app",
    "settings.change_terminal": "components",
    "terminal.terminal": "components",
    "terminal.endpoint": "components",
    "terminal.endpointrule": "components",
    "terminal.status": "components",
    "settings.change_security": "security",
    "settings.change_interface": "appearance",
    "terminal.task": "tasks",
    'ops.celerytask': 'tasks',
    'ops.view_celerytaskexecution': 'tasks',
    'ops.view_taskmonitor': 'tasks',
    "settings.change_clean": "tasks",
    "settings.change_license": "license",
    'orgs.organization': 'view_setting',
    "settings.view_setting": "view_setting",
    "settings.change_other": "other",
}

special_pid_mapper.update(special_setting_pid_mapper)

verbose_name_mapper = {
    'orgs.organization': _("App organizations"),
    'tickets.comment': _("Ticket comment"),
    'tickets.view_ticket': _("Ticket"),
    'settings.setting': _("Common setting"),
    'rbac.view_permission': _('View permission tree'),
    'authentication.passkey': _("Passkey"),
    'oauth2_provider.accesstoken': _("Access token"),
    'oauth2_provider.view_accesstoken': _("View access token"),
    'oauth2_provider.delete_accesstoken': _("Revoke access token"),
}

xpack_nodes = [
    'xpack', 'tickets', 'gather_account_node',
    'applications.remoteapp', "assets.accountbackupplan",
    "assets.accountbackupplanexecution",
    "rbac.orgrole", "rbac.orgrolebinding",
    'assets.gathereduser',
    'settings.change_interface', 'settings.change_sms',
    'users.invite_user', 'users.remove_user',
]


def _sort_action(node):
    if node.isParent:
        return ['zz', 0]

    action_resource = node.title.split('.')[-1]
    action, resource = action_resource.split('_', 2)
    action_value_mapper = {
        'view': 2,
        'add': 4,
        'change': 6,
        'delete': 8
    }
    v = action_value_mapper.get(action, 10)
    return [resource, v]


def sort_nodes(node):
    value = []

    if node.isParent:
        value.append(50)
    else:
        value.append(0)

    value.extend(_sort_action(node))
    return value


class CounterTree(Tree):
    def get_total_count(self, node):
        count = getattr(node, '_total_count', None)
        if count is not None:
            return count

        if not node.data.isParent:
            return 1

        count = 0
        children = self.children(node.identifier)
        for child in children:
            if child.data.isParent:
                count += self.get_total_count(child)
            else:
                count += 1
        node._total_count = count
        return count

    def get_checked_count(self, node):
        count = getattr(node, '_checked_count', None)
        if count is not None:
            return count

        if not node.data.isParent:
            if node.data.checked:
                return 1
            else:
                return 0

        count = 0
        children = self.children(node.identifier)
        for child in children:
            if child.data.isParent:
                count += self.get_checked_count(child)
            else:
                if child.data.checked:
                    count += 1
        node._checked_count = count
        return count

    def add_nodes_to_tree(self, ztree_nodes, retry=0):
        failed = []
        for node in ztree_nodes:
            pid = node.pId
            if retry == 2:
                pid = '$ROOT$'

            try:
                self.create_node(node.name, node.id, pid, data=node)
            except NodeIDAbsentError:
                failed.append(node)
        if retry > 2:
            return
        if failed:
            retry += 1
            return self.add_nodes_to_tree(failed, retry)


class PermissionTreeUtil:
    get_permissions: Callable
    action_mapper = {
        'add': _('Create'),
        'view': _('View'),
        'change': _('Update'),
        'delete': _('Delete')
    }
    action_icon = {
        'add': 'add',
        'view': 'view',
        'change': 'change',
        'delete': 'delete',
        'invite': 'invite',
        'match': 'match',
        'remove': 'remove'
    }

    def __init__(self, permissions, scope, check_disabled=False):
        self.permissions = self.prefetch_permissions(permissions)
        self.all_permissions = self.prefetch_permissions(
            Permission.get_permissions(scope)
        )
        self.check_disabled = check_disabled
        self.lang = get_language()

    @staticmethod
    def prefetch_permissions(perms):
        return perms.select_related('content_type') \
            .annotate(app=F('content_type__app_label')) \
            .annotate(model=F('content_type__model'))

    def create_apps_nodes(self):
        all_apps = apps.get_app_configs()
        apps_name_mapper = {
            app.name: app.verbose_name
            for app in all_apps if hasattr(app, 'verbose_name')
        }
        nodes = []

        for i in app_nodes_data:
            app = i['id']
            name = i.get('name') or apps_name_mapper.get(app, app)
            view = i.get('view', 'other')

            app_data = {
                'id': app,
                'name': name,
                'pId': view,
            }
            node = self._create_node(app_data, 'app', is_open=False)
            nodes.append(node)
        return nodes

    @staticmethod
    def _check_model_xpack(model_id):
        app, model = model_id.split('.', 2)
        if settings.XPACK_ENABLED:
            return True
        if app in xpack_nodes:
            return False
        if model_id in xpack_nodes:
            return False
        return True

    def _create_models_nodes(self):
        content_types = ContentType.objects.all()

        nodes = []
        for ct in content_types:
            model_id = '{}.{}'.format(ct.app_label, ct.model)
            if not self._check_model_xpack(model_id):
                continue

            # 获取 pid
            app = ct.app_label
            if model_id in special_pid_mapper:
                app = special_pid_mapper[model_id]

            # 获取 name
            name = f'{ct.name}'
            if model_id in verbose_name_mapper:
                name = verbose_name_mapper[model_id]

            node = self._create_node({
                'id': model_id,
                'name': name,
                'pId': app,
            }, 'model', is_open=False)
            nodes.append(node)
        return nodes

    def _get_permission_name_icon(self, p: Permission, content_types_name_mapper: dict):
        action, resource = p.codename.split('_', 1)
        icon = self.action_icon.get(action, 'file')
        name = verbose_name_mapper.get(p.app_label_codename)
        if name:
            return name, icon

        app_model = '%s.%s' % (p.content_type.app_label, resource)
        if self.lang == 'en':
            name = p.name
        # 因为默认的权限位是没有翻译的，所以我们要用 action + resource name 去拼
        elif action in self.action_mapper and app_model in content_types_name_mapper:
            action_name = self.action_mapper[action]
            resource_name = content_types_name_mapper[app_model]
            sep = ''
            name = '{}{}{}'.format(action_name, sep, resource_name)
        # 手动创建的 permission
        else:
            name = gettext(p.name)
        name = name.replace('Can ', '').replace('可以', '').capitalize()
        return name, icon

    def _create_perms_nodes(self):
        permissions_id = self.permissions.values_list('id', flat=True)
        nodes = []
        content_types = ContentType.objects.all()
        content_types_name_mapper = {ct.app_model: ct.name for ct in content_types}

        for p in self.all_permissions:
            model_id = f'{p.app}.{p.model}'
            if not self._check_model_xpack(model_id):
                continue
            title = p.app_label_codename
            if not settings.XPACK_ENABLED and title in xpack_nodes:
                continue

            # name 要特殊处理，解决 i18n 问题
            name, icon = self._get_permission_name_icon(p, content_types_name_mapper)
            if settings.DEBUG_DEV:
                name += '[{}]'.format(p.app_label_codename)

            pid = model_id
            # perm node 的特殊设置用的是 title，因为 id 是数字，不一致
            if title in special_pid_mapper:
                pid = special_pid_mapper[title]

            checked = p.id in permissions_id

            node = TreeNode(**{
                'id': p.id,
                'name': name,
                'title': title,
                'pId': pid,
                'isParent': False,
                'chkDisabled': self.check_disabled,
                'iconSkin': icon,
                'checked': checked,
                'open': False,
                'meta': {
                    'type': 'perm',
                }
            })
            nodes.append(node)
        return nodes

    def _create_node(self, data, tp, is_parent=True, is_open=True, icon='', checked=None):
        assert data.get('id')
        assert data.get('name')
        assert data.get('pId') is not None

        node_data = {
            'isParent': is_parent,
            'iconSkin': icon,
            'open': is_open,
            'chkDisabled': self.check_disabled,
            'checked': checked,
            'meta': {
                'type': tp,
            },
            **data
        }
        node_data['title'] = node_data['id']
        node = TreeNode(**node_data)
        if settings.DEBUG_DEV:
            node.name += ('[' + node.id + ']')
        return node

    def _create_root_tree_node(self):
        node = self._create_node(root_node_data, 'root')
        return node

    def _create_views_node(self):
        nodes = []
        for view_data in view_nodes_data:
            data = {
                **view_data,
                'pId': '$ROOT$',
            }
            node = self._create_node(data, 'view', is_open=True)
            nodes.append(node)
        return nodes

    def _create_extra_nodes(self):
        nodes = []
        for data in extra_nodes_data:
            node = self._create_node(data, 'extra', is_open=False)
            nodes.append(node)
        return nodes

    @staticmethod
    def compute_nodes_count(ztree_nodes):
        tree = CounterTree()
        reverse_nodes = ztree_nodes[::-1]
        root = reverse_nodes[0]
        tree.create_node(root.name, root.id, data=root)
        tree.add_nodes_to_tree(reverse_nodes[1:])
        counter_nodes = tree.all_nodes()

        node_counts = {}
        for n in counter_nodes:
            if not n:
                continue
            total_count = tree.get_total_count(n)
            checked_count = tree.get_checked_count(n)
            node_counts[n.identifier] = [checked_count, total_count]

        nodes = []
        for node in ztree_nodes:
            counter = node_counts[node.id]
            if not counter:
                counter = [0, 0]
            checked_count, total_count = counter
            if total_count == 0:
                continue
            if node.isParent:
                node.name += ' ({}/{})'.format(checked_count, total_count)
            if checked_count != 0:
                node.checked = True
            nodes.append(node)
        return nodes

    def create_tree_nodes(self):
        nodes = self._create_perms_nodes()
        nodes += self._create_models_nodes()
        nodes += self.create_apps_nodes()
        nodes += self._create_extra_nodes()
        nodes += self._create_views_node()
        nodes += [self._create_root_tree_node()]

        nodes = self.compute_nodes_count(nodes)
        nodes.sort(key=sort_nodes)
        return nodes
