from json import JSONEncoder, JSONDecoder, loads, dumps, JSONDecodeError

from flask import request, g
from flask_restful import Resource, abort

import lumi2.ldap as ldap
from lumi2.usermodel import User, Group
from lumi2.exceptions import InvalidStringFormatException


def _assert_is_authenticated():
        if not g.is_authenticated:
            abort(401, message="You are not logged in.")


class UserEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, User):
            return {
                "username": obj.username,
                "password_hash": obj.password_hash,
                "email": obj.email,
                "first_name": obj.first_name,
                "last_name": obj.last_name,
                "display_name": obj.display_name,
                "picture": obj.get_picture_url(),
            }

        return JSONEncoder.default(self, obj)


class GroupEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, Group):
            return {
                "groupname": obj.groupname,
                "members": [user.username for user in obj.members],
            }
        return JSONEncoder.default(self, obj)


class UserResource(Resource):
    """The UserResource is used for API access to users."""

    def get(self, username):
        """Returns the specified user in JSON format."""

        _assert_is_authenticated()

        try:
            conn = ldap.get_connection()
        except:
            return 500
        try:
            user = ldap.get_user(conn, username)
        except ldap.EntryNotFoundException:
            return {"message": f"User '{username}' does not exist."}, 400

        return {
            "username": user.username,
            "password_hash": user.password_hash,
            "email": user.email,
            "first_name": user.first_name,
            "last_name": user.last_name,
            "display_name": user.display_name,
            "picture": user.get_picture_url(),
        }, 200


class GroupResource(Resource):
    """The GroupResource represents a Group object in the REST API."""

    def get(self, groupname: str):
        """Retrieves the group specified by the groupname as a JSON object.

        Parameters
        ----------
        groupname : str
            The name of the group to be retrieved.

        Returns
        -------
        json : str , status : int
            A JSON string and HTTP status code.
        """

        _assert_is_authenticated()

        try:
            conn = ldap.get_connection()
        except:
            return 500
        try:
            group = ldap.get_group(conn, groupname)
        except ldap.EntryNotFoundException:
            return {"message": f"Group '{groupname}' does not exist."}, 404

        return {
            "groupname": group.groupname,
            "members": [user.username for user in group.members],
        }, 200


    def post(self, groupname: str):
        """Creates the specified Group with the members listed in the JSON data.

        The request is expected to contain JSON data in the following format:
        {
            "members": [
                "alice",
                "bob",
                "charlie"
            ]
        }

        Returns
        -------
        json : str , status : int
            A JSON string and HTTP status code.
        """

        _assert_is_authenticated()
    
        group_dict = request.get_json()
        if not isinstance(group_dict, dict):
            return {"message": f"Invalid format: expected an object but got: '{type(group_dict)}'."}, 400
        if len(group_dict.keys()) != 1:
            return {"message": "Invalid number of keys in Group object: expected exactly one key."}, 400
        if "members" not in group_dict.keys():
            return {"message": "Expected a key called 'members' in the object."}, 400

        try:
            Group.assert_is_valid_groupname(groupname)
        except InvalidStringFormatException as e:
            return {"message": f"Invalid group name: {e}"}, 400

        member_usernames = group_dict['members']
        if not isinstance(member_usernames, list):
            return {"message": "Expected the value for 'members' to be a list."}, 400
        if not len(member_usernames):
            return {"message": "Group must have at least one member."}, 400
        members = set()

        try:
            conn = ldap.get_connection()
        except:
            return 500

        for username in member_usernames:
            if not isinstance(username, str):
                conn.unbind()
                return {"message": "Member list may contain only strings."}, 400
            try:
                members.add(ldap.get_user(conn, username))
            except ldap.EntryNotFoundException:
                conn.unbind()
                return {"message": f"No such user: '{username}'."}, 400

        group = Group(groupname, members)

        try:
            # Make sure the requested group does not exist yet
            group = ldap.get_group(conn, group.groupname)
            conn.unbind()
            return {"message": f"Group '{group.groupname}' already exists."}, 400
        except ldap.EntryNotFoundException:
            pass

        ldap.create_group(conn, group)
        conn.unbind()
        return {
            "groupname": group.groupname,
            "members": [user.username for user in group.members],
        }, 200


    def delete(self, groupname):
        """Deletes the specified Group.

        Returns
        -------
        json : str , status : int
            A JSON string and HTTP status code.
        """

        _assert_is_authenticated()

        try:
            conn = ldap.get_connection()
        except:
            return 500

        try:
            # Make sure the requested exists
            group = ldap.get_group(conn, groupname)
        except ldap.EntryNotFoundException:
            conn.unbind()
            return {"message": f"Group '{groupname}' does not exist."}, 404

        ldap.delete_group(conn, groupname)
        conn.unbind()
        return None, 200


class GroupMemberResource(Resource):
    """This resource represents the member of a Group."""

    def post(self, groupname: str, username: str):
        """Adds the specified user to the specified Group.

        Parameters
        ----------
        username : str
            The username of the User who will be added to the specified Group.
        groupname : str
            The name of the Group to which the specified User will be added.

        Returns
        -------
        json : str , status : int
            A JSON string and HTTP status code.
            If the request was handled successfully, the  POST-data is
            replied to the client and HTTP code 200 is returned.
            If a failure occurred while processing the request, an appropriate
            error message and HTTP error code are returned.
        """

        _assert_is_authenticated()

        try:
            conn = ldap.get_connection()
        except:
            return 500

        try:
            group = ldap.get_group(conn, groupname)
        except ldap.EntryNotFoundException:
            conn.unbind()
            return {"message": f"Group '{groupname}' does not exist."}, 404

        try:
            user = ldap.get_user(conn, username)
        except ldap.EntryNotFoundException:
            conn.unbind()
            return {"message": f"User '{username}' does not exist."}, 404

        if user in group.members:
            conn.unbind()
            return {"message": f"User '{username}' is already a member of the Group '{group.groupname}'."}, 400

        group.members.add(user)
        ldap.update_group(conn, group)
        conn.unbind()
        return {
            "groupname": group.groupname,
            "members": [user.username for user in group.members],
        }, 200


    def delete(self, groupname: str, username: str):
        """Removes the specified User from the specified Group.

        Parameters
        ----------
        username : str
            The username of the User who will be removed from the specified Group.
        groupname : str
            The name of the Group from which the specified User will be removed.

        Returns
        -------
        json : str , status : int
            A JSON string and HTTP status code.
            If the request was handled successfully, the  POST-data is
            replied to the client and HTTP code 200 is returned.
            If a failure occurred while processing the request, an appropriate
            error message and HTTP error code are returned.
        """

        _assert_is_authenticated()

        try:
            conn = ldap.get_connection()
        except:
            return 500

        try:
            group = ldap.get_group(conn, groupname)
        except ldap.EntryNotFoundException:
            conn.unbind()
            return {"message": f"Group '{groupname}' does not exist."}, 404

        try:
            user = ldap.get_user(conn, username)
        except ldap.EntryNotFoundException:
            conn.unbind()
            return {"message": f"User '{username}' does not exist."}, 404

        if user not in group.members:
            conn.unbind()
            return {"message": f"User '{username}' is not a member of the Group '{group.groupname}'."}, 400

        if len(group.members) == 1:
            conn.unbind()
            return {
                "message": f"Cannot remove user '{username}', because they are currently the only member " \
                           f"of '{group.groupname}'. Empty groups are not permitted in LDAP, so either " \
                           f"delete the group or add another user before removing '{username}'."
            }, 400

        group.members.remove(user)
        ldap.update_group(conn, group)
        conn.unbind()
        return None, 200
