from bulb.contrib.auth.hashers import _hash_password
from bulb.contrib.auth.exceptions import *
from bulb.db.utils import make_uuid
from bulb.db import node_models
from bulb.db.base import gdbh
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.conf import settings
import importlib.util
import warnings
import datetime


BASE_DIR = settings.BASE_DIR


class FakeClass:
    pass


class GroupPermissionRelationship(node_models.Relationship):
    rel_type = "CAN"
    direction = "from"
    start = "self"
    target = "Permission"
    auto = False
    on_delete = "PROTECT"
    unique = False


class UserPermissionRelationship(node_models.Relationship):
    rel_type = "CAN"
    direction = "from"
    start = "self"
    target = "Permission"
    auto = False
    on_delete = "PROTECT"
    unique = False

    def get(self, direction="bi", returned="node", order_by=None, limit=None, skip=None, desc=False, distinct=False, only=None,
            only_user_perms=False):
        user_instance = self._self_node_instance
        if user_instance.is_active_user:
            permissions_list = []

            # Get user's permissions.
            user_permissions = super().get(direction="from", returned="node", order_by=order_by, limit=limit, skip=skip,
                                           desc=desc, distinct=distinct, only=only)

            if user_permissions is not None:
                for permission in user_permissions:
                    permissions_list.append(permission)

            if only_user_perms is False:
                # Get user's groups' permissions.
                groups = user_instance.groups.get(returned="node", direction="from")
                if groups is not None:
                    for group in groups:
                        group_permissions = group.permissions.get(direction="from", returned="node", order_by=order_by,
                                                                  limit=limit, skip=skip, desc=desc, distinct=distinct,
                                                                  only=only)

                        if group_permissions is not None:
                            for perm in group_permissions:
                                if perm not in permissions_list:
                                    permissions_list.append(perm)

            if permissions_list:
                return permissions_list

        return None


class GroupRelationship(node_models.Relationship):
    rel_type = "IS_IN"
    direction = "from"
    start = "self"
    target = "Group"
    auto = False
    on_delete = "PROTECT"
    unique = False


class RelatedUserRelationship(node_models.Relationship):
    rel_type = "IS_IN"
    direction = "to"
    start = "User"
    target = "self"
    auto = False
    on_delete = "PROTECT"
    unique = False


class SessionRelationsip(node_models.Relationship):
    rel_type = "IS_SESSION_OF"
    direction = "to"
    start = "Session"
    target = "self"
    auto = False
    on_delete = "CASCADE"
    unique = True


class Permission(node_models.Node):

    codename = node_models.Property(unique=True,
                                    required=True)

    description = node_models.Property(unique=True,
                                       required=True)

    def __str__(self):
        return f'<Permission object(codename="{self.codename}", description="{self.description}")>'

    def __repr__(self):
        return f'<Permission object(codename="{self.codename}", description="{self.description}")>'

    def __hash__(self):
        return hash(self.codename) + hash(self.description)

    @classmethod
    def get(cls, uuid=None, codename=None, order_by=None, limit=None, skip=None, desc=False, only=None):
        """
        This method allow the retrieving of Permission (or of one of its children classes) instances.


        :param uuid: The Universal Unique Identifier of a permission to get an unique permission instance.

        :param codename: The codename of a permission to get an unique permission instance.

        :param order_by: Must be the name of the property with which the returned datas will be sorted.
                         Examples : "datetime", "first_name", etc...

        :param limit: Must be an integer. This parameter defines the number of returned elements.

        :param skip: Must be an integer. This parameter defines the number of skipped elements. For example if
                     self.skip = 3, the 3 first returned elements will be skipped.

        :param desc: Must be a boolean. If it is False the elements will be returned in an increasing order, but it is
                     True, they will be returned in a descending order.

        :param only: Must be a list of field_names. If this parameter is filled, the return will not be Permission
                     instances, but a dict with "only" the mentioned fields.

        :return: If uuid is None, a list will be returned. Else it will be a unique instance.
        """
        property_statement = ""
        order_by_statement = ""
        limit_statement = ""
        skip_statement = ""
        desc_statement = ""

        # Build the property_statement.
        if uuid is not None and codename is not None:
            property_statement = "{uuid:'%s', codename:'%s'}" % (uuid, codename)

        elif uuid is not None:
            property_statement = "{uuid:'%s'}" % uuid

        elif codename is not None:
            property_statement = "{codename:'%s'}" % codename

        # Build the match_statement.
        cyher_labels = node_models.DatabaseNode.format_labels_to_cypher(cls._get_labels())
        match_statement = f"MATCH (p:{cyher_labels} {property_statement})"

        # Build the with_statement.
        with_statement = "WITH p"

        # Build order_by statements.
        if order_by is not None:
            order_by_statement = f"ORDER BY p.{order_by}"

        # Build return_statement statements.
        if not only:
            return_statement = "RETURN (p)"

        else:
            only_statement_list = []

            for element in only:
                only_statement_list.append(f"p.{element}")

            only_statement = ", ".join(only_statement_list)

            return_statement = f"RETURN {only_statement}"

        # Build limit_statement.
        if limit is not None:
            if not isinstance(limit, str) and not isinstance(limit, int):
                raise BULBGetUserError(
                    f"The 'limit' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                limit_statement = f"LIMIT {limit}"

        # Build skip_statement and add its required variable.
        if skip is not None:
            if not isinstance(skip, str) and not isinstance(skip, int):
                raise BULBGetUserError(
                    f"The 'skip' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                skip_statement = f"SKIP {skip}"

        # Build desc_statement.
        if not isinstance(desc, bool):
            raise BULBGetUserError(
                f"The 'desc' parameter of the get() method of {cls.__name__} must be a boolean.")

        else:
            if desc is True:
                desc_statement = "DESC"

        request_statement = """
            %s
            %s
            %s
            %s
            %s
            %s
            %s
            """ % (match_statement,
                   with_statement,
                   order_by_statement,
                   desc_statement,
                   limit_statement,
                   skip_statement,
                   return_statement)

        response = gdbh.r_transaction(request_statement)

        if response:
            if only is None:
                fake_instances_list = []

                for permission_object in response:
                    fake_instances_list.append(cls.build_fake_instance(permission_object["p"],
                                                                       forced_fake_instance_class=cls))

                if uuid is not None or codename is not None:
                    return fake_instances_list[0]
                else:
                    return fake_instances_list

            else:
                return response

        else:
            return None


def get_permission_node_model():
    if settings.BULB_PERMISSION_NODE_MODEL_FILE == "bulb.contrib.auth.node_models":
        return Permission

    else:
        overloaded_permission = None
        overloaded_permission_module = importlib.import_module(settings.BULB_PERMISSION_NODE_MODEL_FILE)

        try:
            overloaded_permission = overloaded_permission_module.Permission

        except AttributeError:
            warnings.warn(
                f"You have defined BULB_PERMISSION_NODE_MODEL_FILE = '{settings.BULB_PERMISSION_NODE_MODEL_FILE}' but no Permission node model was found in it. So the native Permission node model will be used.",
                BULBPermissionWarning)

        else:
            return overloaded_permission
        return Permission


class Group(node_models.Node):

    name = node_models.Property(unique=True,
                                required=True)

    permissions = GroupPermissionRelationship()

    users = RelatedUserRelationship()

    def __str__(self):
        return f'<Group object(name="{self.name}", uuid="{self.uuid}")>'

    def __repr__(self):
        return f'<Group object(name="{self.name}", uuid="{self.uuid}")>'

    @classmethod
    def get(cls, uuid=None, name=None, order_by=None, limit=None, skip=None, desc=False, only=None):
        """
        This method allow the retrieving of Group (or of one of its children classes) instances.

        :param uuid: The Universal Unique Identifier of a group to get an unique group instance.

        :param name: The name of a group to get an unique group instance.

        :param order_by: Must be the name of the property with which the returned datas will be sorted.
                         Examples : "datetime", "first_name", etc...

        :param limit: Must be an integer. This parameter defines the number of returned elements.

        :param skip: Must be an integer. This parameter defines the number of skipped elements. For example if
                     self.skip = 3, the 3 first returned elements will be skipped.

        :param desc: Must be a boolean. If it is False the elements will be returned in an increasing order, but it is
                     True, they will be returned in a descending order.

        :param only: Must be a list of field_names. If this parameter is filled, the return will not be Group instances,
                     but a dict with "only" the mentioned fields.

        :return: If uuid is None, a list will be returned. Else it will be a unique instance.
        """
        property_statement = ""
        order_by_statement = ""
        limit_statement = ""
        skip_statement = ""
        desc_statement = ""

        # Build the property_statement.
        if uuid is not None and name is not None:
            property_statement = "{uuid:'%s', name:'%s'}" % (uuid, name)

        elif uuid is not None:
            property_statement = "{uuid:'%s'}" % uuid

        elif name is not None:
            property_statement = "{name:'%s'}" % name

        # Build the match_statement.
        cyher_labels = node_models.DatabaseNode.format_labels_to_cypher(cls._get_labels())
        match_statement = f"MATCH (g:{cyher_labels} {property_statement})"

        # Build the with_statement.
        with_statement = "WITH g"

        # Build order_by statements.
        if order_by is not None:
            order_by_statement = f"ORDER BY g.{order_by}"

        # Build return_statement statements.
        if not only:
            return_statement = "RETURN (g)"

        else:
            only_statement_list = []

            for element in only:
                only_statement_list.append(f"g.{element}")

            only_statement = ", ".join(only_statement_list)

            return_statement = f"RETURN {only_statement}"

        # Build limit_statement.
        if limit is not None:
            if not isinstance(limit, str) and not isinstance(limit, int):
                raise BULBGetUserError(
                    f"The 'limit' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                limit_statement = f"LIMIT {limit}"

        # Build skip_statement and add its required variable.
        if skip is not None:
            if not isinstance(skip, str) and not isinstance(skip, int):
                raise BULBGetUserError(
                    f"The 'skip' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                skip_statement = f"SKIP {skip}"

        # Build desc_statement.
        if not isinstance(desc, bool):
            raise BULBGetUserError(
                f"The 'desc' parameter of the get() method of {cls.__name__} must be a boolean.")

        else:
            if desc is True:
                desc_statement = "DESC"

        request_statement = """
            %s
            %s
            %s
            %s
            %s
            %s
            %s
            """ % (match_statement,
                   with_statement,
                   order_by_statement,
                   desc_statement,
                   limit_statement,
                   skip_statement,
                   return_statement)

        response = gdbh.r_transaction(request_statement)

        if response:
            if only is None:
                fake_instances_list = []

                for group_object in response:
                    fake_instances_list.append(cls.build_fake_instance(group_object["g"],
                                                                       forced_fake_instance_class=cls))

                if uuid is not None or name is not None:
                    return fake_instances_list[0]
                else:
                    return fake_instances_list

            else:
                return response

        else:
            return None


def get_group_node_model():
    if settings.BULB_GROUP_NODE_MODEL_FILE == "bulb.contrib.auth.node_models":
        return Group

    else:
        overloaded_group = None
        overloaded_group_module = importlib.import_module(settings.BULB_GROUP_NODE_MODEL_FILE)

        try:
            overloaded_group = overloaded_group_module.Group

        except AttributeError:
            warnings.warn(
                f"You have defined BULB_GROUP_NODE_MODEL_FILE = '{settings.BULB_GROUP_NODE_MODEL_FILE}' but no Group node model was found in it. So the native Group node model will be used.",
                BULBGroupWarning)

        else:
            return overloaded_group
        return Group


class EmptyRelationship(node_models.Relationship):
    rel_type = ""


class AnonymousUser:
    is_super_user = False
    is_staff_user = False
    is_active_user = False
    uuid = None
    first_name = None
    last_name = None
    email = None
    password = None
    registration_datetime = None
    session = EmptyRelationship()
    groups = EmptyRelationship()
    permissions = EmptyRelationship()

    def __str__(self):
        return 'AnonymousUser'

    def __repr__(self):
        return 'AnonymousUser'

    @classmethod
    def create(cls, **extrafields):
        raise NotImplementedError("bulb doesn't provide a DB representation for AnonymousUser.")

    @classmethod
    def create_super_user(cls, **extrafields):
        raise NotImplementedError("bulb doesn't provide a DB representation for AnonymousUser.")

    @classmethod
    def get(cls, uuid_or_email):
        return cls()

    def update(self, user_property, new_user_property_value):
        raise NotImplementedError("bulb doesn't provide a DB representation for AnonymousUser.")

    def delete(self):
        raise NotImplementedError("bulb doesn't provide a DB representation for AnonymousUser.")

    def set_password(self, new_password):
        raise NotImplementedError("bulb doesn't provide a DB representation for AnonymousUser.")

    def has_perm(self, permission_code_name):
        return False


def get_anonymoususer_node_model():
    if settings.BULB_ANONYMOUSUSER_NODE_MODEL_FILE == "bulb.contrib.auth.node_models":
        return AnonymousUser
    else:
        overloaded_anonymoususer = None
        overloaded_anonymoususer_module = importlib.import_module(settings.BULB_ANONYMOUSUSER_NODE_MODEL_FILE)
        try:
            overloaded_anonymoususer = overloaded_anonymoususer_module.AnonymousUser
        except AttributeError:
            warnings.warn(
                f"You have defined BULB_ANONYMOUSUSER_NODE_MODEL_FILE = '{settings.BULB_ANONYMOUSUSER_NODE_MODEL_FILE}' but no AnonymousUser node model was found in it. So the native AnonymousUser node model will be used.",
                BULBAnonymousUserWarning)
        else:
            return overloaded_anonymoususer
        return AnonymousUser


class User(node_models.Node):

    # Default groups
    is_super_user = node_models.Property(default=False)

    is_staff_user = node_models.Property(default=False)

    is_active_user = node_models.Property(default=True)

    first_name = node_models.Property(required=True)

    last_name = node_models.Property(required=True)

    email = node_models.Property(required=True,
                                 unique=True)

    password = node_models.Property(required=True)

    registration_datetime = node_models.Property(default=datetime.datetime.now)

    permissions = UserPermissionRelationship()

    groups = GroupRelationship()

    session = SessionRelationsip()

    email_confirmation_key = None

    email_is_confirmed = None

    if settings.BULB_REGISTRATION_USE_EMAIL_CONFIRMATION:
        email_confirmation_key = node_models.Property()

        email_is_confirmed = node_models.Property(default=False)

    def __str__(self):
        return f'<User object(first_name="{self.first_name}", last_name="{self.last_name}", uuid="{self.uuid}")>'

    def __repr__(self):
        return f'<User object(first_name="{self.first_name}", last_name="{self.last_name}", uuid="{self.uuid}")>'

    @classmethod
    def create(cls, **extrafields):

        extrafields.setdefault('is_super_user', False)
        extrafields.setdefault('is_staff_user', False)
        extrafields.setdefault('is_active_user', True)

        extrafields['password'] = _hash_password(extrafields['password'])

        new_user = cls(**extrafields)

        if settings.BULB_REGISTRATION_USE_EMAIL_CONFIRMATION:
            # Generate the email confirmation key.
            email_confirmation_key = make_uuid()

            # Apply it to the new User instance.
            new_user.update('email_confirmation_key', email_confirmation_key)

            target_email = extrafields[settings.BULB_USER_EMAIL_PROPERTY_NAME]
            confirmation_url = settings.BULB_CONFIRMATION_VIEW_PATH + email_confirmation_key

            send_mail(
                from_email=settings.BULB_EMAIL_SENDER_NAME,
                subject=settings.BULB_EMAIL_SUBJECT,
                recipient_list=[f"{target_email}"],
                message=settings.BULB_EMAIL_DEFAULT_MESSAGE + confirmation_url,
                html_message=render_to_string(settings.BULB_EMAIL_TEMPLATE_PATH,
                                              {"confirmation_url": confirmation_url}))

        return new_user

    @classmethod
    def create_super_user(cls, **extrafields):
        extrafields.setdefault('is_super_user', True)
        extrafields.setdefault('is_staff_user', True)
        extrafields.setdefault('is_active_user', True)

        extrafields['password'] = _hash_password(extrafields['password'])

        is_super_user = extrafields.get('is_super_user')
        is_staff_user = extrafields.get('is_staff_user')
        if not is_super_user:
            raise ValueError('A SuperUser must have "is_super_user = True".')

        elif not is_staff_user:
            raise ValueError('A SuperUser must have "is_staff_user = True".')

        else:
            new_super_user = User(**extrafields)

            return new_super_user

    @classmethod
    def get(cls, uuid=None, email=None, email_confirmation_key=None, order_by=None, limit=None, skip=None, desc=False, only=None):
        """
        This method allow the retrieving of User (or of one of its children classes) instances.

        :param uuid: The Universal Unique Identifier of a user to get an unique user instance.

        :param email: The email of a user to get an unique user instance.

        :param order_by: Must be the name of the property with which the returned datas will be sorted.
                         Examples : "datetime", "first_name", etc...

        :param limit: Must be an integer. This parameter defines the number of returned elements.

        :param skip: Must be an integer. This parameter defines the number of skipped elements. For example if
                     self.skip = 3, the 3 first returned elements will be skipped.

        :param desc: Must be a boolean. If it is False the elements will be returned in an increasing order, but it is
                     True, they will be returned in a descending order.

        :param only: Must be a list of field_names. If this parameter is filled, the return will not be User instances,
                     but a dict with "only" the mentioned fields.

        :return: If uuid is None, a list will be returned. Else it will be a unique instance.
        """
        property_statement = ""
        order_by_statement = ""
        limit_statement = ""
        skip_statement = ""
        desc_statement = ""

        # Build the property_statement.
        if uuid is not None and email is not None:
            property_statement = "{uuid:'%s', email:'%s'}" % (uuid, email)

        elif uuid is not None:
            property_statement = "{uuid:'%s'}" % uuid

        elif email is not None:
            property_statement = "{email:'%s'}" % email

        elif email_confirmation_key is not None:
            property_statement = "{email_confirmation_key:'%s'}" % email_confirmation_key

        # Build the match_statement.
        cyher_labels = node_models.DatabaseNode.format_labels_to_cypher(cls._get_labels())
        match_statement = f"MATCH (u:{cyher_labels} {property_statement})"

        # Build the with_statement.
        with_statement = "WITH u"

        # Build order_by statements.
        if order_by is not None:
            order_by_statement = f"ORDER BY u.{order_by}"

        # Build return_statement statements.
        if not only:
            return_statement = "RETURN (u)"

        else:
            only_statement_list = []

            for element in only:
                only_statement_list.append(f"u.{element}")

            only_statement = ", ".join(only_statement_list)

            return_statement = f"RETURN {only_statement}"

        # Build limit_statement.
        if limit is not None:
            if not isinstance(limit, str) and not isinstance(limit, int):
                raise BULBGetUserError(
                    f"The 'limit' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                limit_statement = f"LIMIT {limit}"

        # Build skip_statement and add its required variable.
        if skip is not None:
            if not isinstance(skip, str) and not isinstance(skip, int):
                raise BULBGetUserError(
                    f"The 'skip' parameter of the get() method of {cls.__name__} must be a string or an integer.")

            else:
                skip_statement = f"SKIP {skip}"

        # Build desc_statement.
        if not isinstance(desc, bool):
            raise BULBGetUserError(
                f"The 'desc' parameter of the get() method of {cls.__name__} must be a boolean.")

        else:
            if desc is True:
                desc_statement = "DESC"

        request_statement = """
               %s
               %s
               %s
               %s
               %s
               %s
               %s
               """ % (match_statement,
                      with_statement,
                      order_by_statement,
                      desc_statement,
                      limit_statement,
                      skip_statement,
                      return_statement)

        response = gdbh.r_transaction(request_statement)

        if response:
            if only is None:

                fake_instances_list = []

                for user_object in response:
                    fake_instances_list.append(cls.build_fake_instance(user_object["u"],
                                                                       forced_fake_instance_class=cls))

                if uuid is not None or email is not None or email_confirmation_key is not None:
                    return fake_instances_list[0]

                else:
                    return fake_instances_list

            else:
                return response

        else:
            if uuid is not None or email is not None or email_confirmation_key is not None:
                if settings.BULB_ANONYMOUSUSER_NODE_MODEL_FILE:
                    AnonymousUser_node_model = get_anonymoususer_node_model()
                    return AnonymousUser_node_model()

            else:
                return None

    def set_password(self, new_password):
        self.update("password", _hash_password(new_password))

    def has_perm(self, permission_code_name):
        if self.is_super_user:
            return True

        if not self.is_active_user:
            return False

        else:
            permission_to_test = Permission.get(codename=permission_code_name)
            all_user_permissions = self.permissions.get()

            if all_user_permissions is not None:
                if permission_to_test in all_user_permissions:
                    return True
            return False


def get_user_node_model():
    if settings.BULB_USER_NODE_MODEL_FILE == "bulb.contrib.auth.node_models":
        return User

    else:
        overloaded_user = None
        overloaded_user_module = importlib.import_module(settings.BULB_USER_NODE_MODEL_FILE)

        try:
            overloaded_user = overloaded_user_module.User

        except AttributeError:
            warnings.warn(
                f"You have defined BULB_USER_NODE_MODEL_FILE = '{settings.BULB_USER_NODE_MODEL_FILE}' but no User node model was found in it. So the native User node model will be used.",
                BULBUserWarning)

        else:
            return overloaded_user
        return User