#

from typing import Union

from django.conf import settings
from django.core.cache import cache
from django.db import migrations, transaction
from rolepermissions.permissions import available_perm_names
from rolepermissions.roles import RolesManager, get_user_roles

ALL_PERMISSIONS = set()
for role in RolesManager.get_roles():
    ALL_PERMISSIONS.update(getattr(role, 'available_permissions', {}).keys())
ALL_PERMISSIONS = frozenset(ALL_PERMISSIONS)
assert ALL_PERMISSIONS  # nosec


class Chainable:
    def __and__(self, other):
        if isinstance(self, All):
            pass
        return All(self, other)

    def __or__(self, other):
        return Any(self, other)


class P(Chainable, str):
    def __new__(cls, permission):
        if not isinstance(permission, (PermissionGroup, P, str)) or str(permission) == '':
            raise ValueError('wrong permission type `{}`'.format(permission))
        return super(P, cls).__new__(cls, permission)


class PermissionGroup(Chainable, tuple):
    def __new__(cls, *permissions):
        errors = []
        for p in permissions:
            if not isinstance(p, (PermissionGroup, P, str)) or str(p) == '':
                errors.append(p)
        if errors:
            raise ValueError('wrong permissions type `{}`'.format('`, `'.join(str(p) for p in errors)))
        return super(PermissionGroup, cls).__new__(cls, permissions)

    def __call__(self, user):
        raise NotImplementedError

    def __bool__(self):
        raise NotImplementedError('PermissionGroup evaluation without a user is meaningless')


class All(PermissionGroup):
    def __call__(self, user):
        return all(has_permission_exactly(user, permission) for permission in self)

    def __repr__(self):
        return '(' + (' & '.join(str(p) for p in self)) + ')'


class Any(PermissionGroup):
    def __call__(self, user):
        return any(has_permission_exactly(user, permission) for permission in self)

    def __repr__(self):
        return '(' + (' | '.join(str(p) for p in self)) + ')'


class AnyWithCache(PermissionGroup):
    def __call__(self, user):
        return any(has_permission_exactly_with_cache(user, permission) for permission in self)

    def __repr__(self):
        return '(' + (' | '.join(str(p) for p in self)) + ')'


def has_permission_exactly(user, permission_name: Union[str, P, PermissionGroup]):
    """Check if a user has a given permission."""
    # wT: dont allow blindly for superuser
    # if user and user.is_superuser:
    #     return True
    if isinstance(permission_name, str):
        return permission_name in available_perm_names(user)
    elif isinstance(permission_name, PermissionGroup):
        return permission_name(user)


def has_permission_exactly_with_cache(user, permission_name: Union[str, P, PermissionGroup]):
    """Check if a user has a given permission."""
    # wT: dont allow blindly for superuser
    # if user and user.is_superuser:
    #     return True
    if isinstance(permission_name, str):
        cached_permission_names = cache.get(f'user_id_{user.id}_permission_names')
        if cached_permission_names:
            return permission_name in cached_permission_names
        else:
            a_perm_names = available_perm_names(user)
            cache.set(f'user_id_{user.id}_permission_names', a_perm_names, timeout=5)
            return permission_name in a_perm_names
    elif isinstance(permission_name, PermissionGroup):
        return permission_name(user)


def grant_perm_names(user):
    grant_permissions = []
    for role in get_user_roles(user):
        available_permissions = getattr(role, 'available_permissions', {})
        for (permission_name, state) in available_permissions.items():
            if state.grant:
                grant_permissions.append(permission_name)
    return grant_permissions


def possible_perm_names(user):
    """
    Return a list of permissions codenames possible to a user, based on that user's roles.
    move Grant permissions to first of list
    """
    grant_permissions = []
    possible_permissions = []
    for role in get_user_roles(user):
        available_permissions = getattr(role, 'available_permissions', {})
        for (permission_name, state) in available_permissions.items():
            if state.grant:
                if permission_name not in grant_permissions:
                    grant_permissions.append(permission_name)
            elif permission_name not in possible_permissions:
                possible_permissions.append(permission_name)

    return grant_permissions + possible_permissions


def rename_permissions(Permission, RENAME_MAP):
    all_permissions = Permission.objects.filter(content_type__app_label='auth', content_type__model='user')
    with transaction.atomic():
        for permission in all_permissions:
            pre_codename = permission.codename
            if pre_codename in RENAME_MAP:
                new_codename = RENAME_MAP[pre_codename]
                if new_codename is None:  # DELETE
                    permission.delete()
                    # print(f'--- deleted {pre_codename}')
                else:  # RENAME
                    permission.codename = new_codename
                    permission.save()
                    # print(f'+++ renamed {pre_codename} -> {new_codename}')
                RENAME_MAP.pop(pre_codename, None)
            elif settings.DEBUG and not (
                    pre_codename.startswith('add_') or  #
                    pre_codename.startswith('view_') or  #
                    pre_codename.startswith('change_') or  #
                    pre_codename.startswith('delete_') or  #
                    pre_codename in ALL_PERMISSIONS
            ):
                print(f'??? unknown {pre_codename}')

        if settings.DEBUG:
            for (k, v) in RENAME_MAP.items():
                print(f'!!! not found so not renamed or removed {k}')
    # exit(1)


def rename_permission_operation(RENAME_MAP, dry_run=False):
    def inner(apps, scheme_editor):
        Permission = apps.get_model('auth', 'Permission')
        rename_permissions(Permission, RENAME_MAP)
        if dry_run:
            exit(-1)

    return migrations.RunPython(inner, migrations.RunPython.noop)
