# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2016-2022 CERN.
# Copyright (C) 2021 Graz University of Technology.
# Copyright (C) 2021 TU Wien.
# Copyright (C) 2022 Northwestern University.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Community permissions."""

import operator
from abc import abstractmethod
from collections import namedtuple
from functools import partial, reduce
from itertools import chain

from flask import current_app
from flask_principal import UserNeed
from invenio_access.permissions import any_user, system_process
from invenio_records_permissions.generators import Generator
from invenio_search.engine import dsl

from .proxies import current_roles

_Need = namedtuple("Need", ["method", "value", "role"])

CommunityRoleNeed = partial(_Need, "community")
"""Defines a need for a community role.

You can create a community role need like below:

.. code-block:: python

    need = CommunityRoleNeed(community.id, 'manager')
    # Community role needs can be identified by their method attribute
    assert need.method == 'community
    # The community identifier can be accessed via the value attribute
    assert need.value == community.id
    # The role held within the community can be accessed via the role attribute
    assert need.role == 'manager
"""


# TODO: Move class to Invenio-Records-Permissions and make more reusable
class IfRestrictedBase(Generator):
    """IfRestricted generator.

    IfRestricted(
        'record', 'restricted', 'public'
        then_=[RecordPermissionLevel('view')],
        else_=[ActionNeed(superuser-access)],
    )
    """

    def __init__(self, field_getter, field_name, then_value, else_value, then_, else_):
        """Constructor."""
        self.field_getter = field_getter
        self.field_name = field_name
        self.then_value = then_value
        self.else_value = else_value
        self.then_ = then_
        self.else_ = else_

    def generators(self, record):
        """Get the "then" or "else" generators."""
        if record is None:
            # TODO - when permissions on links are checked, the record is not
            # passes properly, causing below ``record.access`` to fail.
            return self.else_
        # TODO: Next statement should be made more reusable
        is_then_value = self.field_getter(record) or self.then_value
        return self.then_ if is_then_value == self.then_value else self.else_

    def needs(self, record=None, **kwargs):
        """Set of Needs granting permission."""
        needs = [g.needs(record=record, **kwargs) for g in self.generators(record)]
        return set(chain.from_iterable(needs))

    def excludes(self, record=None, **kwargs):
        """Set of Needs denying permission."""
        needs = [g.excludes(record=record, **kwargs) for g in self.generators(record)]
        return set(chain.from_iterable(needs))

    def make_query(self, generators, **kwargs):
        """Make a query for one set of generators."""
        queries = [g.query_filter(**kwargs) for g in generators]
        queries = [q for q in queries if q]
        return reduce(operator.or_, queries) if queries else None

    def query_filter(self, **kwargs):
        """Filters for current identity."""
        # TODO: Next two statement should be made more reusable
        q_then = dsl.Q("match", **{self.field_name: self.then_value})
        q_else = dsl.Q("match", **{self.field_name: self.else_value})
        then_query = self.make_query(self.then_, **kwargs)
        else_query = self.make_query(self.else_, **kwargs)

        if then_query and else_query:
            return (q_then & then_query) | (q_else & else_query)
        elif then_query:
            return (q_then & then_query) | q_else
        elif else_query:
            return q_else & else_query
        else:
            return q_else


# TODO: Remove class once IfRestrictedBase has been moved.
class IfRestricted(IfRestrictedBase):
    """IfRestricted."""

    def __init__(self, field, then_, else_):
        """Initialize."""
        super().__init__(
            lambda r: getattr(r.access, field, None),
            f"access.{field}",
            "restricted",
            "public",
            then_,
            else_,
        )


class IfPolicyClosed(IfRestrictedBase):
    """If policy is closed."""

    def __init__(self, field, then_, else_):
        """Initialize."""
        super().__init__(
            lambda r: getattr(r.access, field, None)
            if hasattr(r, "access")
            else r.get("access", {}).get(
                field
            ),  # needed for running permission check at serialization time and avoid db query
            f"access.{field}",
            "closed",
            "open",
            then_,
            else_,
        )


#
# Community membership generators
#
class CommunityRoles(Generator):
    """Base class for community roles generators."""

    def roles(self, **kwargs):
        """R."""
        raise NotImplementedError

    def communities(self, identity):
        """Communities."""
        raise NotImplementedError

    def needs(self, record=None, community_id=None, **kwargs):
        """Enabling Needs."""
        if community_id is None:
            community_id = record.id

        assert community_id, "No community id provided."
        community_id = str(community_id)

        roles = self.roles(**kwargs)
        if roles:
            needs = [CommunityRoleNeed(community_id, role) for role in roles]
            return needs
        return []

    def query_filter(self, identity=None, **kwargs):
        """Filters for current identity as owner."""
        # Gives access to all community members.
        return dsl.Q("terms", **{"_id": self.communities(identity)})


class CommunityMembers(CommunityRoles):
    """Roles representing all members of a community."""

    def roles(self, **kwargs):
        """Roles."""
        return [r.name for r in current_roles]

    def communities(self, identity):
        """Communities."""
        return [n.value for n in identity.provides if n.method == "community"]


class CommunityCurators(CommunityRoles):
    """Roles representing all curators of a community."""

    def roles(self, **kwargs):
        """Roles."""
        return [r.name for r in current_roles.can("curate")]


class CommunityManagers(CommunityRoles):
    """Roles representing all managers of a community."""

    def roles(self, **kwargs):
        """Roles."""
        return [r.name for r in current_roles.can("manage")]


class CommunityManagersForRole(CommunityRoles):
    """Roles representing all managers of a community for a role update."""

    def roles(self, role=None, member=None, **kwargs):
        """Roles."""
        allowed_roles = []
        if role is not None and member is not None:
            # Update from an old role to a new role. The most restrictive set
            # applies.
            new_allowed_roles = set(current_roles.manager_roles(role))
            old_allowed_roles = set(current_roles.manager_roles(member.role))
            allowed_roles = new_allowed_roles & old_allowed_roles
        elif role is not None:
            # Adding/inviting a new role
            allowed_roles = current_roles.manager_roles(role)
        elif member is not None:
            # Deleting or updating a member with a given role (without changing
            # role)
            allowed_roles = current_roles.manager_roles(member.role)
        else:
            raise NotImplementedError("You must provide a role and/or a member.")

        return [r.name for r in allowed_roles]


class CommunityOwners(CommunityRoles):
    """Roles representing the owners of a community."""

    def roles(self, **kwargs):
        """Roles."""
        return [current_roles.owner_role.name]

    def communities(self, identity):
        """Communities."""
        return [
            n.value
            for n in identity.provides
            if n.method == "community" and n.role == current_roles.owner_role.name
        ]


class CommunitySelfMember(Generator):
    """Allows a member themselves (only for users)."""

    def needs(self, member=None, **kwargs):
        """Enabling needs."""
        if member is not None and member.user_id is not None:
            return [UserNeed(member.user_id)]
        return []

    def query_filter(self, identity=None, **kwargs):
        """Not implemented."""
        raise NotImplementedError("Search permission not supported.")


#
# Business-level rules.
#
class AllowedMemberTypes(Generator):
    """Generator to restrict to set of allowed member types.

    We are restricting member types that can be added/invited via permissions
    so that it's easily overridable for an instance to change behavior.

    A system process is allowed to do anything.
    """

    def __init__(self, *allowed_member_types):
        """Allowed member types."""
        self.allowed_member_types = allowed_member_types

    def needs(self, **kwargs):
        """Enabling needs."""
        return [system_process]

    def excludes(self, member_types=None, **kwargs):
        """Preventing needs."""
        if member_types:
            for m in member_types:
                if m not in self.allowed_member_types:
                    return [any_user]
        return []


class GroupsEnabled(Generator):
    """Generator to restrict if the groups are not enabled.

    If the groups are not enabled, exclude any user for adding members of the
    param member type.

    A system process is allowed to do anything.
    """

    def __init__(self, *need_groups_enabled_types):
        """Types that need the groups enabled."""
        self.need_groups_enabled_types = need_groups_enabled_types

    def excludes(self, member_types=None, **kwargs):
        """Preventing needs."""
        if member_types:
            for m in member_types:
                if (
                    m in self.need_groups_enabled_types
                    and not current_app.config["COMMUNITIES_GROUPS_ENABLED"]
                ):
                    return [any_user]
        return []


# TODO https://github.com/inveniosoftware/invenio-rdm-records/issues/1226
class ConditionalGenerator(Generator):
    """Generator that depends on whether a condition is true or not.

    If...(
        then_=[...],
        else_=[...],
    )
    """

    def __init__(self, then_, else_):
        """Constructor."""
        self.then_ = then_
        self.else_ = else_

    @abstractmethod
    def _condition(self, **kwargs):
        """Condition to choose generators set."""
        raise NotImplementedError()

    def _generators(self, record, **kwargs):
        """Get the "then" or "else" generators."""
        return self.then_ if self._condition(record=record, **kwargs) else self.else_

    def needs(self, record=None, **kwargs):
        """Set of Needs granting permission."""
        needs = [
            g.needs(record=record, **kwargs) for g in self._generators(record, **kwargs)
        ]
        return set(chain.from_iterable(needs))

    def excludes(self, record=None, **kwargs):
        """Set of Needs denying permission."""
        excludes = [
            g.excludes(record=record, **kwargs)
            for g in self._generators(record, **kwargs)
        ]
        return set(chain.from_iterable(excludes))


class IfConfig(ConditionalGenerator):
    """Config-based conditional generator."""

    def __init__(self, config_key, accept_values=[True], **kwargs):
        """Initialize generator."""
        self.accept_values = accept_values
        self.config_key = config_key
        super().__init__(**kwargs)

    def _condition(self, **_):
        """Check if the config value is truthy."""
        return current_app.config.get(self.config_key) in self.accept_values
