"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21

"""

import uuid
from abc import abstractmethod

from django.contrib import messages
from django.db.models import QuerySet

from konova.sub_settings.lanis_settings import LANIS_LINK_TEMPLATE, LANIS_LINK_DEFAULT
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
    celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
    celery_send_mail_shared_data_deleted, celery_send_mail_shared_data_checked, \
    celery_send_mail_shared_access_given_team, celery_send_mail_shared_access_removed_team, \
    celery_send_mail_shared_data_checked_team, celery_send_mail_shared_data_deleted_team, \
    celery_send_mail_shared_data_unrecorded_team, celery_send_mail_shared_data_recorded_team
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.utils.timezone import now
from django.db import models, transaction
from konova.utils import generators
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import GEOMETRY_CONFLICT_WITH_TEMPLATE


class UuidModel(models.Model):
    """
    Encapsules identifying via uuid
    """
    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False,
    )

    class Meta:
        abstract = True


class BaseResource(UuidModel):
    """
    A basic resource model, which defines attributes for every derived model
    """
    created = models.ForeignKey(
        "user.UserActionLogEntry",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='+'
    )
    modified = models.ForeignKey(
        "user.UserActionLogEntry",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='+',
        help_text="Last modified"
    )

    class Meta:
        abstract = True

    def delete(self, using=None, keep_parents=False):
        """ Base deleting of a resource

        Args:
            using ():
            keep_parents ():

        Returns:

        """
        try:
            self.created.delete()
        except (ObjectDoesNotExist, AttributeError):
            # Object does not exist anymore - we can skip this
            pass
        super().delete()


class DeletableObjectMixin(models.Model):
    """ Wraps deleted field and related functionality

    """
    deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')

    class Meta:
        abstract = True

    def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
        """ Mark an entry as deleted

        Does not delete from database but sets a timestamp for being deleted on and which user deleted the object

        Args:
            user (User): The performing user
            send_mail (bool): Whether to send mails to users and teams
            comment (str|None): The comment for delete action

        Returns:

        """
        from user.models import UserActionLogEntry
        if self.deleted:
            # Nothing to do here
            return

        with transaction.atomic():
            action = UserActionLogEntry.get_deleted_action(user, comment)
            self.deleted = action
            self.log.add(action)

            if send_mail:
                # Send mail
                shared_users = self.shared_users.values_list("id", flat=True)
                for user_id in shared_users:
                    celery_send_mail_shared_data_deleted.delay(self.id, self.get_app_object_tuple(), user_id)

                # Send mail
                shared_teams = self.shared_teams.values_list("id", flat=True)
                for team_id in shared_teams:
                    celery_send_mail_shared_data_deleted_team.delay(self.id, self.get_app_object_tuple(), team_id)

            self.save()


class BaseObject(BaseResource, DeletableObjectMixin):
    """
    A basic object model, which specifies BaseResource.

    Mainly used for intervention, compensation, ecoaccount
    """
    identifier = models.CharField(max_length=1000, null=True, blank=True)
    title = models.CharField(max_length=1000, null=True, blank=True)
    comment = models.TextField(null=True, blank=True)
    log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)

    identifier_length = 6                   # Fallback - specified in inheriting classes
    identifier_template = "UNBEKANNT-{}"    # Fallback - specified in inheriting classes

    class Meta:
        abstract = True

    def delete(self, using=None, keep_parents=False):
        """ Base deleting of a BaseObject instance

        Args:
            using:
            keep_parents:

        Returns:

        """
        self.__delete_external_identifier()
        super().delete(using, keep_parents)

    def __delete_external_identifier(self):
        """ Checks on existing external identifier linking and drops them

        Returns:

        """
        from api.models import ExternalIdentifier
        try:
            external_identifier = ExternalIdentifier.objects.filter(internal_id=self.id)
            external_identifier.delete()
        except ObjectDoesNotExist:
            pass


    @abstractmethod
    def set_status_messages(self, request: HttpRequest):
        raise NotImplementedError

    def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
        """ In case the object or a related object changed the log history needs to be updated

        Args:
            performing_user (User): The user which performed the editing action
            request (HttpRequest): The used request for this action
            edit_comment (str): Additional comment for the log entry

        Returns:

        """
        from user.models import UserActionLogEntry
        edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
        self.modified = edit_action
        self.log.add(edit_action)
        self.save()
        return edit_action

    def add_log_entry(self, action, user, comment: str):
        """ Wraps adding of UserActionLogEntry to log

        Args:
            action (UserAction): The performed UserAction
            user (User): Performing user
            comment (str): The optional comment

        Returns:

        """
        from user.models import UserActionLogEntry
        user_action = UserActionLogEntry.objects.create(
            user=user,
            action=action,
            comment=comment
        )
        self.log.add(user_action)

    def generate_new_identifier(self) -> str:
        """ Generates a new identifier for the intervention object

        Returns:
            str
        """
        id_len = self.identifier_length
        id_template = self.identifier_template

        _now = now()
        curr_month = _now.month
        if curr_month < 10:
            # Make sure month part has two digits to have same length identifiers by default
            curr_month = f"0{curr_month}"
        else:
            curr_month = str(curr_month)
        curr_year = str(_now.year)
        rand_str = generate_random_string(
            length=id_len,
            use_numbers=True,
            use_letters_lc=False,
            use_letters_uc=True,
        )
        _str = "{}{}-{}".format(curr_month, curr_year, rand_str)
        return id_template.format(_str)

    @abstractmethod
    def get_detail_url(self):
        raise NotImplementedError()

    def get_app_object_tuple(self):
        """ Getter for a tuple, consisting of app and object name of the object

        Returns:
            tuple (tuple)
        """
        return (self._meta.app_label, self._meta.object_name)


class RecordableObjectMixin(models.Model):
    """ Wraps record related fields and functionality

    """
    # Refers to "verzeichnen"
    recorded = models.OneToOneField(
        "user.UserActionLogEntry",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Holds data on user and timestamp of this action",
        related_name="+"
    )

    class Meta:
        abstract = True

    def set_unrecorded(self, user):
        """ Perform unrecording

        Args:
            user (User): Performing user

        Returns:

        """
        from user.models import UserActionLogEntry
        if not self.recorded:
            return None
        action = UserActionLogEntry.get_unrecorded_action(user)
        self.recorded = None
        self.save()
        self.log.add(action)

        shared_users = self.shared_users.values_list("id", flat=True)
        shared_teams = self.shared_teams.values_list("id", flat=True)

        for user_id in shared_users:
            celery_send_mail_shared_data_unrecorded.delay(self.id, self.get_app_object_tuple(), user_id)

        for team_id in shared_teams:
            celery_send_mail_shared_data_unrecorded_team.delay(self.id, self.get_app_object_tuple(), team_id)

        return action

    def set_recorded(self, user):
        """ Perform recording

        Args:
            user (User): Performing user

        Returns:

        """
        from user.models import UserActionLogEntry
        if self.recorded:
            return None

        self.unshare_with_default_users()
        action = UserActionLogEntry.get_recorded_action(user)
        self.recorded = action
        self.save()
        self.log.add(action)

        shared_users = self.shared_users.values_list("id", flat=True)
        shared_teams = self.shared_teams.values_list("id", flat=True)

        for user_id in shared_users:
            celery_send_mail_shared_data_recorded.delay(self.id, self.get_app_object_tuple(), user_id)

        for team_id in shared_teams:
            celery_send_mail_shared_data_recorded_team.delay(self.id, self.get_app_object_tuple(), team_id)

        return action

    @abstractmethod
    def is_ready_for_publish(self) -> bool:
        """ Check for all needed publishing-constraints on the data

        Returns:
            is_ready (bool): True|False
        """
        raise NotImplementedError("Implement this in the subclass!")

    @property
    def is_recorded(self):
        """ Getter for record status as property

        Returns:

        """
        return self.recorded is not None


class CheckableObjectMixin(models.Model):
    # Checks - Refers to "Genehmigen" but optional
    checked = models.OneToOneField(
        "user.UserActionLogEntry",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Holds data on user and timestamp of this action",
        related_name="+"
    )

    class Meta:
        abstract = True

    def set_unchecked(self) -> None:
        """ Perform unchecking

        Args:

        Returns:

        """
        if not self.checked:
            # Nothing to do
            return
        # Do not .delete() the checked attribute! Just set it to None, since a delete() would remove it from the
        # log history, which is not what we want!
        self.checked = None
        self.save()
        return None

    def set_checked(self, user):
        """ Perform checking

        Args:
            user (User): Performing user

        Returns:

        """
        from user.models import UserActionLogEntry
        if self.checked:
            # Nothing to do
            return
        action = UserActionLogEntry.get_checked_action(user)
        self.checked = action
        self.save()

        # Send mail
        shared_users = self.shared_users.values_list("id", flat=True)
        for user_id in shared_users:
            celery_send_mail_shared_data_checked.delay(self.id, self.get_app_object_tuple(), user_id)

        # Send mail
        shared_teams = self.shared_teams.values_list("id", flat=True)
        for team_id in shared_teams:
            celery_send_mail_shared_data_checked_team.delay(self.id, self.get_app_object_tuple(), team_id)

        self.log.add(action)
        return action

    def get_last_checked_action(self):
        """ Getter for the most recent checked action on the log

        Returns:
            previously_checked (UserActionLogEntry): The most recent checked action
        """
        from user.models import UserAction
        previously_checked = self.log.filter(
            action=UserAction.CHECKED
        ).order_by(
            "-timestamp"
        ).first()
        return previously_checked


class ShareableObjectMixin(models.Model):
    # Users having access on this object
    users = models.ManyToManyField(
        "user.User",
        help_text="Users having access (data shared with)",
        blank=True
    )
    teams = models.ManyToManyField(
        "user.Team",
        help_text="Teams having access (data shared with)",
        blank=True
    )
    access_token = models.CharField(
        max_length=255,
        null=True,
        blank=True,
        help_text="Used for sharing access",
    )

    class Meta:
        abstract = True

    def generate_access_token(self, make_unique: bool = False, rec_depth: int = 5):
        """ Creates a new access token for the data

        Tokens are not used for identification of a table row. The share logic checks the intervention id as well
        as the given token. Therefore two different interventions can hold the same access_token without problems.
        For (possible) future changes to the share logic, the make_unique parameter may be used for checking whether
        the access_token is already used in any intervention. If so, tokens will be generated as long as a free token
        can be found.

        Args:
            make_unique (bool): Perform check on uniqueness over all intervention entries
            rec_depth (int): How many tries for generating a free random token (only if make_unique)

        Returns:

        """
        # Make sure we won't end up in an infinite loop of trying to generate access_tokens
        rec_depth = rec_depth - 1
        if rec_depth < 0 and make_unique:
            raise RuntimeError(
                "Access token generating for {} does not seem to find a free random token! Aborted!".format(self.id)
            )

        # Create random token
        token = generators.generate_random_string(15, True, True, False)
        # Check dynamically wheter there is another instance of that model, which holds this random access token
        _model = self._meta.concrete_model
        token_used_in = _model.objects.filter(access_token=token)
        # Make sure the token is not used anywhere as access_token, yet.
        # Make use of QuerySet lazy method for checking if it exists or not.
        if token_used_in and make_unique:
            self.generate_access_token(make_unique, rec_depth)
        else:
            self.access_token = token
            self.save()

    def is_shared_with(self, user):
        """ Access check

        Checks whether a given user has access to this object

        Args:
            user ():

        Returns:

        """
        obj_shared_teams = self.shared_teams
        obj_shared_users = self.shared_users
        user_shared_teams = user.shared_teams

        directly_shared = obj_shared_users.filter(id=user.id).exists()
        team_shared = (obj_shared_teams & user_shared_teams).exists()

        is_shared = directly_shared or team_shared
        return is_shared

    def is_only_shared_with(self, user):
        """ Sharing check

        Checks whether a given user is the only shared user for this object.
        There should be no shared teams as well.

        Args:
            user ():

        Returns:

        """
        has_shared_teams = self.shared_teams.exists()
        shared_users = self.shared_users
        is_only_shared_user = user in shared_users and shared_users.count() == 1

        return not has_shared_teams and is_only_shared_user

    def share_with_team(self, team):
        """ Adds team to list of shared access teans

        Args:
            team (Team): The team to be added to the object

        Returns:

        """
        self.teams.add(team)

    def share_with_team_list(self, team_list: list):
        """ Sets the list of shared access teams

        Args:
            team_list (list): The teams to be added to the object

        Returns:

        """
        self.teams.set(team_list)

    def share_with_user(self, user):
        """ Adds user to list of shared access users

        Args:
            user (User): The user to be added to the object

        Returns:

        """
        if not self.is_shared_with(user):
            self.users.add(user)

    def share_with_user_list(self, user_list: list):
        """ Sets the list of shared access users

        Args:
            user_list (list): The users to be added to the object

        Returns:

        """
        self.users.set(user_list)

    def _update_shared_teams(self, form):
        """ Updates shared access on the object for teams

        Args:
            form (ShareModalForm): The form holding the data

        Returns:

        """
        form_data = form.cleaned_data
        shared_teams = self.shared_teams

        # Fetch selected teams and find out which user IDs are in removed teams -> mails need to be sent
        accessing_teams = form_data["teams"]
        removed_teams = shared_teams.exclude(
            id__in=accessing_teams
        ).values_list("id", flat=True)
        new_teams = accessing_teams.exclude(
            id__in=shared_teams
        ).values_list("id", flat=True)

        for team_id in new_teams:
            celery_send_mail_shared_access_given_team.delay(self.id, self.get_app_object_tuple(),  team_id)
        for team_id in removed_teams:
            celery_send_mail_shared_access_removed_team.delay(self.id, self.get_app_object_tuple(), team_id)

        self.share_with_team_list(accessing_teams)

    def _update_shared_users(self, form):
        """ Updates shared access on the object for single users

        Args:
            form (ShareModalForm): The form holding the data

        Returns:

        """
        form_data = form.cleaned_data
        shared_users = self.shared_users

        # Fetch selected users
        accessing_users = form_data["users"]
        removed_users = shared_users.exclude(
            id__in=accessing_users
        ).values_list("id", flat=True)
        new_users = accessing_users.exclude(
            id__in=shared_users
        ).values_list("id", flat=True)

        # Send mails
        for user_id in removed_users:
            celery_send_mail_shared_access_removed.delay(self.id, self.get_app_object_tuple(), user_id)
        for user_id in new_users:
            celery_send_mail_shared_access_given.delay(self.id, self.get_app_object_tuple(), user_id)

        # Set new shared users
        self.share_with_user_list(accessing_users)

    def update_shared_access(self, form):
        """ Updates shared access on the object

        Args:
            form (ShareModalForm): The form holding the data

        Returns:

        """
        self._update_shared_teams(form)
        self._update_shared_users(form)

    @property
    def shared_users(self) -> QuerySet:
        """ Shortcut for fetching the users which have shared access on this object

        Returns:
            users (QuerySet)
        """
        return self.users.all()

    @property
    def shared_teams(self) -> QuerySet:
        """ Shortcut for fetching the teams which have shared access on this object

        Returns:
            teams (QuerySet)
        """
        return self.teams.filter(
            deleted__isnull=True
        )

    @abstractmethod
    def get_share_url(self):
        """ Returns the share url for the object

        Returns:

        """
        raise NotImplementedError("Must be implemented in subclasses!")

    def unshare_with_default_users(self):
        """ Removes all shared users from direct shared access which are only default group users

        Returns:

        """
        users = self.shared_users
        cleaned_users = []
        default_users = []
        for user in users:
            if not user.is_default_group_only():
                cleaned_users.append(user)
            else:
                default_users.append(user)
        self.share_with_user_list(cleaned_users)

        for user in default_users:
            celery_send_mail_shared_access_removed.delay(self.id, self.get_app_object_tuple(), user.id)


class GeoReferencedMixin(models.Model):
    geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)

    class Meta:
        abstract = True

    def get_underlying_parcels(self):
        """ Getter for related parcels

        Returns:
            parcels (Iterable): An empty list or a Queryset
        """
        result = []
        if self.geometry is not None:
            result = self.geometry.get_underlying_parcels()
        return result

    def set_geometry_conflict_message(self, request: HttpRequest):
        if self.geometry is None:
            return request

        conflicting_geometries = self.geometry.get_conflict_geometries()
        data_object_identifiers = []
        for conflicting_geom in conflicting_geometries:
            data_obj_id = conflicting_geom.get_data_object(["identifier"])
            if data_obj_id:
                data_object_identifiers.append(data_obj_id)

        add_message = len(data_object_identifiers) > 0
        if add_message:
            data_object_identifiers = [x["identifier"] for x in data_object_identifiers]
            instance_identifiers = ", ".join(data_object_identifiers)
            message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers)
            messages.info(request, message_str)
        return request

    def get_LANIS_link(self) -> str:
        """ Creates the link for 'LANIS' button on detail view of an entry.

        Returns:

        """
        try:
            entry_has_geometry = not self.geometry.geom.empty
        except AttributeError:
            entry_has_geometry = False

        if entry_has_geometry:
            link = LANIS_LINK_TEMPLATE.format(
                self.get_lanis_layer_name(),
                self.identifier
            )
        else:
            link = LANIS_LINK_DEFAULT
        return link

    def get_lanis_layer_name(self):
        raise NotImplementedError("Must be implemented in subclasses!")


class ResubmitableObjectMixin(models.Model):
    resubmissions = models.ManyToManyField(
        "konova.Resubmission",
        blank=True,
        related_name="+",
    )

    class Meta:
        abstract = True

    def resubmit(self):
        """ Run resubmit check and run for all related resubmissions

        """
        resubmissions = self.resubmissions.all()
        for resubmission in resubmissions:
            resubmission.send_resubmission_mail(self.id, self.get_app_object_tuple())
