# -*- coding: utf-8 -*-
import logging

from flask import request, g
from flask_restful import Api
from http import HTTPStatus

from vantage6.server import db
from vantage6.server.resource.common.pagination import Pagination
from vantage6.server.resource.common.input_schema import (
    CollaborationAddNodeSchema,
    CollaborationAddOrganizationSchema,
    CollaborationInputSchema
)
from vantage6.server.permission import (
    RuleCollection,
    Scope as S,
    Operation as P,
    PermissionManager
)
from vantage6.server.resource.common.output_schema import (
    CollaborationSchema,
    OrganizationSchema,
    NodeSchemaSimple
)
from vantage6.server.resource import (
    with_user,
    only_for,
    ServicesResources
)


module_name = __name__.split('.')[-1]
log = logging.getLogger(module_name)


def setup(api: Api, api_base: str, services: dict) -> None:
    """
    Setup the collaboration resource.

    Parameters
    ----------
    api : Api
        Flask restful api instance
    api_base : str
        Base url of the api
    services : dict
        Dictionary with services required for the resource endpoints
    """
    path = "/".join([api_base, module_name])
    log.info(f'Setting up "{path}" and subdirectories')

    api.add_resource(
        Collaborations,
        path,
        endpoint='collaboration_without_id',
        methods=('GET', 'POST'),
        resource_class_kwargs=services
    )
    api.add_resource(
        Collaboration,
        path + '/<int:id>',
        endpoint='collaboration_with_id',
        methods=('GET', 'PATCH', 'DELETE'),
        resource_class_kwargs=services
    )
    api.add_resource(
        CollaborationOrganization,
        path+'/<int:id>/organization',
        endpoint='collaboration_with_id_organization',
        methods=('POST', 'DELETE'),
        resource_class_kwargs=services
    )
    api.add_resource(
        CollaborationNode,
        path+'/<int:id>/node',
        endpoint='collaboration_with_id_node',
        methods=('POST', 'DELETE'),
        resource_class_kwargs=services
    )


# Schemas
collaboration_schema = CollaborationSchema()
org_schema = OrganizationSchema()
node_schema = NodeSchemaSimple()
collaboration_input_schema = CollaborationInputSchema()
collaboration_add_organization_schema = CollaborationAddOrganizationSchema()
collaboration_add_node_schema = CollaborationAddNodeSchema()


# -----------------------------------------------------------------------------
# Permissions
# -----------------------------------------------------------------------------
def permissions(permissions: PermissionManager) -> None:
    """
    Define the permissions for this resource.

    Parameters
    ----------
    permissions : PermissionManager
        Permission manager instance to which permissions are added
    """

    add = permissions.appender(module_name)

    add(scope=S.GLOBAL, operation=P.VIEW,
        description="view any collaboration")

    add(scope=S.ORGANIZATION, operation=P.VIEW, assign_to_container=True,
        assign_to_node=True,
        description="view collaborations of your organization")

    add(scope=S.GLOBAL, operation=P.EDIT,
        description="edit any collaboration")
    add(scope=S.COLLABORATION, operation=P.EDIT,
        description="edit any collaboration that your organization "
                    "participates in")

    add(scope=S.GLOBAL, operation=P.CREATE,
        description="create a new collaboration")

    add(scope=S.GLOBAL, operation=P.DELETE,
        description="delete a collaboration")
    add(scope=S.COLLABORATION, operation=P.DELETE,
        description="delete any collaboration that your organization "
                    "participates in")


# ------------------------------------------------------------------------------
# Resources / API's
# ------------------------------------------------------------------------------
class CollaborationBase(ServicesResources):

    def __init__(self, socketio, mail, api, permissions, config):
        super().__init__(socketio, mail, api, permissions, config)
        self.r: RuleCollection = getattr(self.permissions, module_name)


class Collaborations(CollaborationBase):

    @only_for(['user', 'node'])
    def get(self):
        """Returns a list of collaborations
        ---
        description: >-
          Returns a list of collaborations. Depending on your permission, all
          collaborations are shown or only collaborations in which your
          organization participates. See the table bellow.\n\n

          ### Permission Table\n
          |Rulename|Scope|Operation|Assigned to Node|Assigned to Container|
          Description|\n
          | -- | -- | -- | -- | -- | -- |\n
          |Collaboration|Global|View|❌|❌|All collaborations|\n
          |Collaboration|Organization|View|✅|✅|Collaborations in which
          your organization participates |\n\n

          Accessible to users.

        parameters:
          - in: query
            name: name
            schema:
              type: string
            description: >-
              Name to match with a LIKE operator. \n
              * The percent sign (%) represents zero, one, or multiple
              characters\n
              * underscore sign (_) represents one, single character
          - in: query
            name: encrypted
            schema:
              type: boolean
            description: Whether or not collaboration is encrypted
          - in: query
            name: organization_id
            schema:
              type: integer
            description: Organization id
          - in: query
            name: page
            schema:
              type: integer
            description: Page number for pagination (default=1)
          - in: query
            name: per_page
            schema:
              type: integer
            description: Number of items per page (default=10)
          - in: query
            name: sort
            schema:
              type: string
            description: >-
              Sort by one or more fields, separated by a comma. Use a minus
              sign (-) in front of the field to sort in descending order.

        responses:
          200:
            description: Ok
          401:
            description: Unauthorized
          400:
            description: Improper values for pagination or sorting parameters

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """

        # obtain organization from authenticated
        auth_org_id = self.obtain_organization_id()
        q = g.session.query(db.Collaboration)
        args = request.args

        # filter by a field of this endpoint
        if 'encrypted' in args:
            q = q.filter(db.Collaboration.encrypted == args['encrypted'])
        if 'name' in args:
            q = q.filter(db.Collaboration.name.like(args['name']))

        # find collaborations containing a specific organization
        if 'organization_id' in args:
            if not self.r.v_glo.can() and \
                    args['organization_id'] != str(auth_org_id):
                return {
                    'msg': 'You lack the permission to request collaborations '
                    'for this organization!'
                }, HTTPStatus.UNAUTHORIZED
            elif self.r.v_glo.can():
                q = q.join(db.Member).join(db.Organization)\
                    .filter(db.Organization.id == args['organization_id'])
            # else: no filter if user can only view collaborations of own
            # organization: the arg 'organization_id' is then superfluous

        # filter based on permissions
        if not self.r.v_glo.can():
            if self.r.v_org.can():
                q = q.join(db.Organization, db.Collaboration.organizations)\
                    .filter(db.Collaboration.organizations.any(id=auth_org_id))
            else:
                return {'msg': 'You lack the permission to do that!'}, \
                    HTTPStatus.UNAUTHORIZED

        # paginate the results
        try:
            page = Pagination.from_query(q, request)
        except ValueError as e:
            return {'msg': str(e)}, HTTPStatus.BAD_REQUEST

        # serialize models
        return self.response(page, collaboration_schema)

    @with_user
    def post(self):
        """ Create collaboration
        ---
        description: >-
          Create a new collaboration between organizations.\n\n

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to Node|Assigned to
          Container|Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Create|❌|❌|Create collaboration|\n\n

          Accessible to users.

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  name:
                    type: string
                    description: Unique human readable name for collaboration
                  organization_ids:
                    type: array
                    items:
                      type: integer
                      description: List of organization ids which form the
                        collaboration
                  encrypted:
                    type: integer
                    description: Boolean (0 or 1) to indicate if the
                      collaboration uses encryption

        responses:
          200:
            description: Ok
          400:
            description: Collaboration name already exists
          401:
            description: Unauthorized

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """
        data = request.get_json()
        # validate request body
        errors = collaboration_input_schema.validate(data)
        if errors:
            return {'msg': 'Request body is incorrect', 'errors': errors}, \
                HTTPStatus.BAD_REQUEST

        name = data["name"]
        if db.Collaboration.exists("name", name):
            return {"msg": f"Collaboration name '{name}' already exists!"}, \
                HTTPStatus.BAD_REQUEST

        if not self.r.c_glo.can():
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        encrypted = True if data["encrypted"] == 1 else False

        collaboration = db.Collaboration(
            name=name,
            organizations=[
                db.Organization.get(org_id)
                for org_id in data['organization_ids']
                if db.Organization.get(org_id)
            ],
            encrypted=encrypted
        )

        collaboration.save()
        return collaboration_schema.dump(collaboration), HTTPStatus.OK


class Collaboration(CollaborationBase):

    @only_for(('user', 'node', 'container'))
    def get(self, id):
        """ Get collaboration
        ---
        description: >-
          Returns the collaboration with the specified id.\n

          ### Permission Table\n
          |Rulename|Scope|Operation|Assigned to Node|Assigned to Container|
          Description|\n
          | -- | -- | -- | -- | -- | -- |\n
          |Collaboration|Global|View|❌|❌|All collaborations|\n
          |Collaboration|Organization|View|✅|✅|Collaborations in which
          your organization participates |\n\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
              minimum: 1
            description: Collaboration id
            required: true

        responses:
          200:
            description: Ok
          404:
            description: Collaboration with specified id is not found
          401:
            description: Unauthorized

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """
        collaboration = db.Collaboration.get(id)

        # check that collaboration exists, unlikely to happen without ID
        if not collaboration:
            return {"msg": f"collaboration having id={id} not found"},\
                HTTPStatus.NOT_FOUND

        # obtain the organization id of the authenticated
        auth_org_id = self.obtain_organization_id()

        # verify that the user/node organization is within the
        # collaboration
        ids = [org.id for org in collaboration.organizations]
        if not self.r.v_glo.can():
            if not (self.r.v_org.can() and auth_org_id in ids):
                return {'msg': 'You lack the permission to do that!'}, \
                    HTTPStatus.UNAUTHORIZED

        return collaboration_schema.dump(collaboration, many=False), \
            HTTPStatus.OK  # 200

    @with_user
    def patch(self, id):
        """ Update collaboration
        ---
        description: >-
          Updates the collaboration with the specified id.\n\n
          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Edit|❌|❌|Update a collaboration|\n\n
          |Collaboration|Collaboration|Edit|❌|❌|Update a collaboration that
          you are already a member of|\n\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id
            required: tr

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  name:
                    type: string
                    description: Human readable label
                  organization_ids:
                    type: array
                    items:
                      type: integer
                    description: List of organization ids
                  encrypted:
                    type: boolean
                    description: Whether collaboration is encrypted or not

        responses:
          200:
            description: Ok
          404:
            description: Collaboration with specified id is not found
          401:
            description: Unauthorized
          400:
            description: Collaboration name already exists

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """
        collaboration = db.Collaboration.get(id)

        # check if collaboration exists
        if not collaboration:
            return {"msg": f"collaboration having collaboration_id={id} "
                    "can not be found"}, HTTPStatus.NOT_FOUND  # 404

        # verify permissions
        if not self.r.can_for_col(P.EDIT, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        data = request.get_json()
        # validate request body
        errors = collaboration_input_schema.validate(data, partial=True)
        if errors:
            return {'msg': 'Request body is incorrect', 'errors': errors}, \
                HTTPStatus.BAD_REQUEST

        # only update fields that are provided
        if "name" in data:
            name = data["name"]
            if collaboration.name != name and \
                    db.Collaboration.exists("name", name):
                return {
                    "msg": f"Collaboration name '{name}' already exists!"
                }, HTTPStatus.BAD_REQUEST
            collaboration.name = name
        if "organization_ids" in data:
            collaboration.organizations = [
                db.Organization.get(org_id)
                for org_id in data['organization_ids']
                if db.Organization.get(org_id)
            ]
        if 'encrypted' in data:
            collaboration.encrypted = data['encrypted']

        collaboration.save()

        return collaboration_schema.dump(collaboration, many=False), \
            HTTPStatus.OK  # 200

    @with_user
    def delete(self, id):
        """ Delete collaboration
        ---
        description: >-
          Removes the collaboration from the database entirely.

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Delete|❌|❌|Remove collaboration|\n\n
          |Collaboration|Collaboration|Delete|❌|❌|Remove collaborations
          that you are part of yourself|\n\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id
            required: true
          - in: query
            name: delete_dependents
            schema:
              type: boolean
            description: If set to true, the collaboratio will be deleted along
              with all its tasks and nodes (default=False)

        responses:
          200:
            description: Ok
          404:
            description: Collaboration with specified id is not found
          401:
            description: Unauthorized

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """

        collaboration = db.Collaboration.get(id)
        if not collaboration:
            return {"msg": f"collaboration id={id} is not found"}, \
                HTTPStatus.NOT_FOUND

        # verify permissions
        if not self.r.can_for_col(P.DELETE, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        if collaboration.tasks or collaboration.nodes:
            delete_dependents = request.args.get('delete_dependents', False)
            if not delete_dependents:
                return {
                    "msg": f"Collaboration id={id} has "
                    f"{len(collaboration.tasks)} tasks and "
                    f"{len(collaboration.nodes)} nodes. Please delete them "
                    "separately or set delete_dependents=True"
                }, HTTPStatus.BAD_REQUEST
            else:
                log.warn(f"Deleting collaboration id={id} along with "
                         f"{len(collaboration.tasks)} tasks and "
                         f"{len(collaboration.nodes)} nodes")
                for task in collaboration.tasks:
                    task.delete()
                for node in collaboration.nodes:
                    node.delete()

        collaboration.delete()
        return {"msg": f"Collaboration id={id} successfully deleted"}, \
            HTTPStatus.OK


class CollaborationOrganization(ServicesResources):
    """Resource for /api/collaboration/<int:id>/organization."""

    def __init__(self, socketio, mail, api, permissions, config):
        super().__init__(socketio, mail, api, permissions, config)
        self.r: RuleCollection = getattr(self.permissions, module_name)

    @with_user
    def post(self, id):
        """ Add organization to collaboration
        ---
        description: >-
          Adds a single organization to an existing collaboration.\n\n

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Edit|❌|❌|Add organization to a
          collaboration|\n\n
          |Collaboration|Collaboration|Edit|❌|❌|Add organization to a
          collaboration that your organization is already a member of|\n\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id
            required: tr

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  id:
                    type: integer
                    description: Organization id which needs to be added

        responses:
          200:
            description: Ok
          404:
            description: Specified collaboration or organization does not exist
          401:
            description: Unauthorized

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """
        # get collaboration to which te organization should be added
        collaboration = db.Collaboration.get(id)
        if not collaboration:
            return {"msg": f"collaboration having collaboration_id={id} can "
                    "not be found"}, HTTPStatus.NOT_FOUND

        # verify permissions
        if not self.r.can_for_col(P.EDIT, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        # validate request body
        data = request.get_json()
        errors = collaboration_add_organization_schema.validate(data)
        if errors:
            return {'msg': 'Request body is incorrect', 'errors': errors}, \
                HTTPStatus.BAD_REQUEST

        # get the organization
        organization = db.Organization.get(data['id'])
        if not organization:
            return {"msg": f"organization with id={id} is not found"}, \
                HTTPStatus.NOT_FOUND

        # append organization to the collaboration
        collaboration.organizations.append(organization)
        collaboration.save()
        return org_schema.dump(collaboration.organizations, many=True), \
            HTTPStatus.OK

    @with_user
    def delete(self, id):
        """ Remove organization from collaboration
        ---
        description: >-
          Removes a single organization from an existing collaboration.\n

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Edit|❌|❌|Remove an organization from an
          existing collaboration|\n\n
          |Collaboration|Collaboration|Edit|❌|❌|Remove an organization from
          an existing collaboration that your organization is a member of|\n\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id
            required: true

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  id:
                    type: integer
                    description: Organization id which needs to be deleted

        responses:
          200:
            description: Ok
          404:
            description: Specified collaboration or organization does not exist
          401:
            description: Unauthorized

        tags: ["Collaboration"]
        """
        # get collaboration from which organization should be removed
        collaboration = db.Collaboration.get(id)
        if not collaboration:
            return {"msg": f"Collaboration with collaboration_id={id} can "
                    "not be found"}, HTTPStatus.NOT_FOUND

        # get organization which should be deleted
        data = request.get_json()
        organization = db.Organization.get(data['id'])
        if not organization:
            return {"msg": f"Organization with id={id} is not found"}, \
                HTTPStatus.NOT_FOUND

        if not self.r.can_for_col(P.EDIT, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        # delete organization and update
        collaboration.organizations.remove(organization)
        collaboration.save()
        return org_schema.dump(collaboration.organizations, many=True), \
            HTTPStatus.OK


class CollaborationNode(ServicesResources):
    """Resource for /api/collaboration/<int:id>/node."""

    def __init__(self, socketio, mail, api, permissions, config):
        super().__init__(socketio, mail, api, permissions, config)
        self.r: RuleCollection = getattr(self.permissions, module_name)

    @with_user
    def post(self, id):
        """ Add node to collaboration
        ---
        description: >-
          Add node to an existing collaboration.\n

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Edit|❌|❌|Add node to collaboration|\n
          |Collaboration|Collaboration|Edit|❌|❌|Add node to collaboration
          that your organization is a member of|\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id
            required: tr

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  id:
                    type: integer
                    description: ID of node to be added

        responses:
          201:
            description: Added node to collaboration
          404:
            description: Collaboration or node not found
          400:
            description: Node is already in collaboration
          401:
            description: Unauthorized

        security:
          - bearerAuth: []

        tags: ["Collaboration"]
        """
        collaboration = db.Collaboration.get(id)
        if not collaboration:
            return {"msg": f"collaboration having collaboration_id={id} can "
                    "not be found"}, HTTPStatus.NOT_FOUND

        if not self.r.can_for_col(P.EDIT, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        # validate request body
        data = request.get_json()
        errors = collaboration_add_node_schema.validate(data)
        if errors:
            return {'msg': 'Request body is incorrect', 'errors': errors}, \
                HTTPStatus.BAD_REQUEST

        node = db.Node.get(data['id'])
        if not node:
            return {"msg": f"node id={data['id']} not found"}, \
                HTTPStatus.NOT_FOUND

        if node in collaboration.nodes:
            return {"msg": f"node id={data['id']} is already in collaboration "
                    f"id={id}"}, HTTPStatus.BAD_REQUEST
        elif node.organization not in collaboration.organizations:
            return {
                "msg": f"Node id={data['id']} belongs to an organization that "
                f"is not part of collaboration id={id}. Please add the "
                "organization to the collaboration first"
            }, HTTPStatus.BAD_REQUEST

        collaboration.nodes.append(node)
        collaboration.save()
        return node_schema.dump(collaboration.nodes, many=True),\
            HTTPStatus.CREATED

    @with_user
    def delete(self, id):
        """ Remove node from collaboration
        ---
        description: >-
          Removes a single node from an existing collaboration.

          ### Permission Table\n
          |Rule name|Scope|Operation|Assigned to node|Assigned to container|
          Description|\n
          |--|--|--|--|--|--|\n
          |Collaboration|Global|Edit|❌|❌|Remove node from collaboration|\n
          |Collaboration|Collaboration|Edit|❌|❌|Remove node from
          collaboration that your organization is a member of|\n

          Accessible to users.

        parameters:
          - in: path
            name: id
            schema:
              type: integer
            description: Collaboration id from which the node is to be deleted.
            required: true

        requestBody:
          content:
            application/json:
              schema:
                properties:
                  id:
                    type: integer
                    description: Node id which needs to be deleted

        responses:
          200:
            description: Ok
          404:
            description: Collaboration or node not found
          400:
            description: Node is not part of the collaboration
          401:
            description: Unauthorized

        tags: ["Collaboration"]
        """
        collaboration = db.Collaboration.get(id)
        if not collaboration:
            return {"msg": f"collaboration having collaboration_id={id} can "
                    "not be found"}, HTTPStatus.NOT_FOUND

        if not self.r.can_for_col(P.EDIT, collaboration.id):
            return {'msg': 'You lack the permission to do that!'}, \
                HTTPStatus.UNAUTHORIZED

        data = request.get_json()
        node = db.Node.get(data['id'])
        if not node:
            return {"msg": f"node id={id} not found"}, HTTPStatus.NOT_FOUND

        if node not in collaboration.nodes:
            return {"msg": f"node id={data['id']} is not part of "
                    f"collaboration id={id}"}, HTTPStatus.BAD_REQUEST

        collaboration.nodes.remove(node)
        collaboration.save()
        return {"msg": f"node id={data['id']} removed from collaboration "
                f"id={id}"}, HTTPStatus.OK
