import json
import logging

from mongoengine.errors import DoesNotExist, ValidationError
from passlib.apps import custom_app_context

import brew_view
from bg_utils.mongo.models import Principal, Role
from bg_utils.mongo.parser import MongoParser
from brew_view.authorization import (
    authenticated,
    check_permission,
    Permissions,
    coalesce_permissions,
)
from brew_view.base_handler import BaseHandler
from brewtils.errors import ModelValidationError


class UserAPI(BaseHandler):

    logger = logging.getLogger(__name__)

    def get(self, user_identifier):
        """
        ---
        summary: Retrieve a specific User
        parameters:
          - name: user_identifier
            in: path
            required: true
            description: The ID or name of the User
            type: string
        responses:
          200:
            description: User with the given ID
            schema:
              $ref: '#/definitions/User'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Users
        """
        if user_identifier == "anonymous":
            principal = brew_view.anonymous_principal
        else:
            # Need fine-grained access control here
            if user_identifier not in [
                self.current_user.id,
                self.current_user.username,
            ]:
                check_permission(self.current_user, [Permissions.USER_READ])

            try:
                principal = Principal.objects.get(id=str(user_identifier))
            except (DoesNotExist, ValidationError):
                principal = Principal.objects.get(username=str(user_identifier))

        principal.permissions = coalesce_permissions(principal.roles)[1]

        self.write(MongoParser.serialize_principal(principal, to_string=False))

    @authenticated(permissions=[Permissions.USER_DELETE])
    def delete(self, user_id):
        """
        ---
        summary: Delete a specific User
        parameters:
          - name: user_id
            in: path
            required: true
            description: The ID of the User
            type: string
        responses:
          204:
            description: User has been successfully deleted
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Users
        """
        principal = Principal.objects.get(id=str(user_id))
        principal.delete()

        self.set_status(204)

    def patch(self, user_id):
        """
        ---
        summary: Partially update a User
        description: |
          The body of the request needs to contain a set of instructions
          detailing the updates to apply:
          ```JSON
          {
            "operations": [
              { "operation": "add", "path": "/roles", "value": "admin" }
            ]
          }
          ```
        parameters:
          - name: user_id
            in: path
            required: true
            description: The ID of the User
            type: string
          - name: patch
            in: body
            required: true
            description: Instructions for how to update the User
            schema:
              $ref: '#/definitions/Patch'
        responses:
          200:
            description: User with the given ID
            schema:
              $ref: '#/definitions/User'
          400:
            $ref: '#/definitions/400Error'
          404:
            $ref: '#/definitions/404Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Users
        """
        principal = Principal.objects.get(id=str(user_id))
        operations = MongoParser.parse_patch(
            self.request.decoded_body, many=True, from_string=True
        )

        for op in operations:
            if op.path == "/roles":
                check_permission(self.current_user, [Permissions.USER_UPDATE])

                try:
                    if op.operation == "add":
                        principal.roles.append(Role.objects.get(name=op.value))
                    elif op.operation == "remove":
                        principal.roles.remove(Role.objects.get(name=op.value))
                    elif op.operation == "set":
                        principal.roles = [
                            Role.objects.get(name=name) for name in op.value
                        ]
                    else:
                        raise ModelValidationError(
                            "Unsupported operation '%s'" % op.operation
                        )
                except DoesNotExist:
                    raise ModelValidationError("Role '%s' does not exist" % op.value)

            elif op.path == "/preferences/theme":
                if user_id != self.current_user.id:
                    check_permission(self.current_user, [Permissions.USER_UPDATE])

                if op.operation == "set":
                    principal.preferences["theme"] = op.value
                else:
                    raise ModelValidationError(
                        "Unsupported operation '%s'" % op.operation
                    )

            else:
                check_permission(self.current_user, [Permissions.USER_UPDATE])
                raise ModelValidationError("Unsupported path '%s'" % op.path)

        principal.save()

        self.write(MongoParser.serialize_principal(principal, to_string=False))


class UsersAPI(BaseHandler):
    @authenticated(permissions=[Permissions.USER_READ])
    def get(self):
        """
        ---
        summary: Retrieve all Users
        responses:
          200:
            description: All Users
            schema:
              type: array
              items:
                $ref: '#/definitions/User'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Users
        """
        principals = Principal.objects.all().select_related(max_depth=1)

        for principal in principals:
            principal.permissions = coalesce_permissions(principal.roles)[1]

        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(
            MongoParser.serialize_principal(principals, to_string=True, many=True)
        )

    @authenticated(permissions=[Permissions.USER_CREATE])
    def post(self):
        """
        ---
        summary: Create a new User
        parameters:
          - name: user
            in: body
            description: The user
            schema:
              type: object
              properties:
                username:
                  type: string
                  description: the name
                password:
                  type: string
                  description: the password
              required:
                - username
                - password
        consumes:
          - application/json
        responses:
          201:
            description: A new User has been created
            schema:
              $ref: '#/definitions/User'
          400:
            $ref: '#/definitions/400Error'
          50x:
            $ref: '#/definitions/50xError'
        tags:
          - Users
        """
        parsed = json.loads(self.request.decoded_body)

        user = Principal(
            username=parsed["username"],
            hash=custom_app_context.hash(parsed["password"]),
        )
        user.save()

        self.set_status(204)
