from django.contrib.auth.models import User
from resources.models import (
    Project,
    Role,
    ProjectAccess,
    ObjectPermission,
    Resource,
    Collection,
    File,
    Metadata,
    MetadataSet,
    Tag,
    ProjectProperty,
)
from typing import Iterator, Union, List
from rpc.errors import ServiceException
from rpc.const import NO_ACCESS
import logging

logger = logging.getLogger(__file__)
PERM_CLASS_RESOURCE = "resource"
PERM_CLASS_COLLECTION = "collection"
PERM_CLASS_FILE = "file"
PERM_CLASS_METADATA = "metadata"
PERM_CLASS_METADATASET = "metadataset"
PERM_CLASS_PROJECT_PROPERTY = "project_property"
PERM_CLASS_TAG = "tag"
PERM_CLASSES = [
    PERM_CLASS_RESOURCE,
    PERM_CLASS_COLLECTION,
    PERM_CLASS_METADATA,
    PERM_CLASS_METADATASET,
    PERM_CLASS_PROJECT_PROPERTY,
    PERM_CLASS_TAG,
]
CRUD_CREATE = "create"
CRUD_READ = "read"
CRUD_UPDATE = "update"
CRUD_DELETE = "delete"
CRUD_READ_VERBS = [CRUD_READ]
CRUD_WRITE_VERBS = [CRUD_CREATE, CRUD_UPDATE, CRUD_DELETE]
CRUD_VERBS = [CRUD_CREATE, CRUD_READ, CRUD_UPDATE, CRUD_DELETE]
REJECTION_LOGGING_MESSAGE = "ACL rejected {} access to user {} for item {}"


def _climb_tree(
    object_instance: Union[Resource, Collection],
) -> Iterator[Union[Resource, Collection]]:
    """
    Traverse from the leaf Resource or Collection to the root Collection
    """
    yield object_instance
    for collec in object_instance.ancestors(reverse=False):
        yield collec


def _class_name_from_instance(object_instance: Union[str, Resource, Collection]) -> str:
    # if str, considered to be already a class name
    if type(object_instance) is str:
        return object_instance
    if isinstance(object_instance, Resource):
        return PERM_CLASS_RESOURCE
    if isinstance(object_instance, Collection):
        return PERM_CLASS_COLLECTION
    if isinstance(object_instance, File):
        return PERM_CLASS_RESOURCE  # File == Resource
    if isinstance(
        object_instance, Metadata
    ):  # no actual use, only metadataset is used.
        return PERM_CLASS_METADATA
    if isinstance(object_instance, MetadataSet):
        return PERM_CLASS_METADATASET
    if isinstance(object_instance, ProjectProperty):
        return PERM_CLASS_PROJECT_PROPERTY
    if isinstance(object_instance, Tag):
        return PERM_CLASS_TAG
    raise ValueError(f"Unsupported object: {object_instance}")


def _crud_val(permission: ObjectPermission, crud_verb: str) -> bool:
    if crud_verb not in CRUD_VERBS:
        raise ValueError(
            f"not a crud access right: {crud_verb}. Must be one of {', '.join(CRUD_VERBS)}."
        )
    if crud_verb == CRUD_CREATE:
        return permission.object_create
    if crud_verb == CRUD_READ:
        return permission.object_read
    if crud_verb == CRUD_UPDATE:
        return permission.object_update
    if crud_verb == CRUD_DELETE:
        return permission.object_delete
    return False


def has_crud_access(
    user_permissions: List[ObjectPermission],
    object_instance: Union[str, Resource, Collection],
    crud_access: str,
) -> bool:
    crud_access = str(crud_access).lower().strip()
    if crud_access not in CRUD_VERBS:
        raise ValueError(
            f"not a crud access right: {crud_access}. Must be one of {', '.join(CRUD_VERBS)}."
        )
    # Only Resource and Collection are tested for instance-level permissions
    if len(
        [x.object_pk for x in user_permissions if x.object_pk is not None]
    ):  # are there any permissions linked to object instances ?
        if isinstance(object_instance, Resource) or isinstance(
            object_instance, Collection
        ):
            # node is either a Resource or a Collection instance
            for node in _climb_tree(object_instance):
                tested_permissions_boolean_values = []
                for permission in user_permissions:
                    if (
                        permission.object_class == _class_name_from_instance(node)
                        and permission.object_pk == node.pk
                    ):
                        tested_permissions_boolean_values.append(
                            _crud_val(permission, crud_access)
                        )
                if len(tested_permissions_boolean_values):
                    return False not in tested_permissions_boolean_values

    # test class-level permissions
    for permission in user_permissions:
        if (
            permission.object_class == _class_name_from_instance(object_instance)
            and permission.object_pk is None
        ):
            return _crud_val(permission, crud_access)

    return False


def set_permission_on_role(
    role: Role,
    object_or_class,
    can_create: bool = False,
    can_read: bool = False,
    can_update: bool = False,
    can_delete: bool = False,
):
    object_class_name = _class_name_from_instance(object_or_class)
    object_pk = None
    if isinstance(object_or_class, Resource) or isinstance(object_or_class, Collection):
        object_pk = object_or_class.pk
    ObjectPermission.objects.filter(
        role=role,
        object_class=object_class_name,
        object_pk=object_pk,
    ).delete()
    ObjectPermission.objects.create(
        role=role,
        object_class=object_class_name,
        object_pk=object_pk,
        object_create=can_create,
        object_read=can_read,
        object_update=can_update,
        object_delete=can_delete,
    )


def remove_permissions_from_role(role: Role):
    ObjectPermission.objects.filter(role=role).delete()


def make_or_create_global_readonly_role_for_project(project: Project) -> Role:
    role, created = Role.objects.get_or_create(
        label="project read only", project=project
    )
    if created:
        for PERM_CLASS in PERM_CLASSES:
            ObjectPermission.objects.create(
                role=role,
                object_class=PERM_CLASS,
                object_pk=None,
                object_create=False,
                object_read=True,
                object_update=False,
                object_delete=True,
            )
    return role


def make_or_create_global_readwrite_role_for_project(project: Project) -> Role:
    role, created = Role.objects.get_or_create(
        label="project read/write", project=project
    )
    if created:
        for PERM_CLASS in PERM_CLASSES:
            ObjectPermission.objects.create(
                role=role,
                object_class=PERM_CLASS,
                object_pk=None,
                object_create=True,
                object_read=True,
                object_update=True,
                object_delete=True,
            )
    return role


def make_or_create_global_noaccess_role_for_project(project: Project) -> Role:
    role, created = Role.objects.get_or_create(
        label="project no access", project=project
    )
    if created:
        for PERM_CLASS in PERM_CLASSES:
            ObjectPermission.objects.create(
                role=role,
                object_class=PERM_CLASS,
                object_pk=None,
                object_create=False,
                object_read=False,
                object_update=False,
                object_delete=False,
            )
    return role


def set_role_on_project(project: Project, role_label: str) -> Role:
    role, _ = Role.objects.get_or_create(project=project, label=role_label)
    return role


class UserAccess:
    def __init__(self, user: User, project: Project):
        self.user = user
        self.project = project
        self._permissions = None

    @property
    def permissions(self) -> List[ObjectPermission]:
        """
        Gets all rows of ObjectPermission tied to the user.

        (keeps a cached copy in the ACL instance)
        """
        if self._permissions is None:
            self._permissions = []
            for perm in ObjectPermission.objects.filter(
                role__projectaccess__user=self.user,
                role__projectaccess__project=self.project,
            ):
                self._permissions.append(perm)
        return self._permissions

    def can_crud(self, object_or_class, crud_access) -> bool:
        return (
            has_crud_access(self.permissions, object_or_class, crud_access)
            and self.user.is_active
        )

    def can_create(self, object_or_class) -> bool:
        return (
            has_crud_access(self.permissions, object_or_class, CRUD_CREATE)
            and self.user.is_active
        )

    def can_read(self, object_or_class) -> bool:
        if self.user.is_superuser and self.user.is_active:  # superuser gets a pass
            return True
        return (
            has_crud_access(self.permissions, object_or_class, CRUD_READ)
            and self.user.is_active
        )

    def can_update(self, object_or_class) -> bool:
        return (
            has_crud_access(self.permissions, object_or_class, CRUD_UPDATE)
            and self.user.is_active
        )

    def can_delete(self, object_or_class) -> bool:
        return (
            has_crud_access(self.permissions, object_or_class, CRUD_DELETE)
            and self.user.is_active
        )

    def check_create(self, object_or_class):
        """
        Check if user can create item and raise ServiceException if not
        """
        if not self.can_create(object_or_class):
            logger.info(
                REJECTION_LOGGING_MESSAGE.format(
                    CRUD_CREATE, self.user.username, object_or_class
                )
            )
            raise ServiceException(NO_ACCESS.format(CRUD_CREATE, object_or_class))

    def check_read(self, object_or_class):
        """
        Check if user can read item and raise ServiceException if not
        """
        if not self.can_read(object_or_class):
            logger.info(
                REJECTION_LOGGING_MESSAGE.format(
                    CRUD_READ, self.user.username, object_or_class
                )
            )
            raise ServiceException(NO_ACCESS.format(CRUD_READ, object_or_class))

    def check_update(self, object_or_class):
        """
        Check if user can update item and raise ServiceException if not
        """
        if not self.can_update(object_or_class):
            logger.info(
                REJECTION_LOGGING_MESSAGE.format(
                    CRUD_UPDATE, self.user.username, object_or_class
                )
            )
            raise ServiceException(NO_ACCESS.format(CRUD_UPDATE, object_or_class))

    def check_delete(self, object_or_class):
        """
        Check if user can delete item and raise ServiceException if not
        """
        if not self.can_delete(object_or_class):
            logger.info(
                REJECTION_LOGGING_MESSAGE.format(
                    CRUD_DELETE, self.user.username, object_or_class
                )
            )
            raise ServiceException(NO_ACCESS.format(CRUD_DELETE, object_or_class))

    def user_roles(self) -> Iterator[Role]:
        for access in ProjectAccess.objects.filter(
            project=self.project, user=self.user
        ):
            yield access.role

    def project_roles(self) -> Iterator[Role]:
        for role in Role.objects.filter(project=self.project):
            yield role

    def add_role_to_user(self, role: Role) -> ProjectAccess:
        if role.project.pk != self.project.pk:
            raise ValueError("Role does not belong to Project")
        access, _ = ProjectAccess.objects.get_or_create(
            project=self.project, user=self.user, role=role
        )
        self._permissions = None
        return access

    def remove_role_from_user(self, role: Role):
        if role.project.pk != self.project.pk:
            raise ValueError("Role does not belong to Project")
        ProjectAccess.objects.filter(
            project=self.project, user=self.user, role=role
        ).delete()
        self._permissions = None
