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

"""
import shutil

from django.contrib import messages
from user.models import User
from django.db import models, transaction
from django.db.models import QuerySet
from django.http import HttpRequest

from compensation.models import EcoAccountDeduction
from intervention.managers import InterventionManager
from intervention.models.legal import Legal
from intervention.models.responsibility import Responsibility
from intervention.models.revocation import RevocationDocument, Revocation
from intervention.utils.quality import InterventionQualityChecker
from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
    ShareableObjectMixin, \
    RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION
from user.models import UserActionLogEntry


class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin):
    """
    Interventions are e.g. construction sites where nature used to be.
    """
    responsible = models.OneToOneField(
        Responsibility,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')"
    )
    legal = models.OneToOneField(
        Legal,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Holds data on legal dates or law"
    )

    objects = InterventionManager()

    def __str__(self):
        return "{} ({})".format(self.identifier, self.title)

    def save(self, *args, **kwargs):
        """ Custom save functionality

        Performs some pre-save checks:
            1. Checking for existing identifiers

        Args:
            *args ():
            **kwargs ():

        Returns:

        """
        if self.identifier is None or len(self.identifier) == 0:
            # No identifier given by the user
            self.identifier = self.generate_new_identifier()

        # Before saving, make sure the given identifier is not used in the meanwhile
        while Intervention.objects.filter(identifier=self.identifier).exclude(id=self.id).exists():
            self.identifier = self.generate_new_identifier()
        super().save(*args, **kwargs)

    def delete(self, using=None, keep_parents=False):
        to_delete = [
            self.legal,
            self.responsible,
            self.geometry,
            self.log.all()
        ]
        for entry in to_delete:
            try:
                entry.delete()
            except AttributeError:
                pass
        super().delete(using, keep_parents)

    def quality_check(self) -> InterventionQualityChecker:
        """ Quality check

        Returns:
            ret_msgs (list): Holds error messages
        """
        checker = InterventionQualityChecker(obj=self)
        checker.run_check()
        return checker

    def get_LANIS_link(self) -> str:
        """ Generates a link for LANIS depending on the geometry

        Returns:

        """
        try:
            geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
            x = geom.centroid.x
            y = geom.centroid.y
            area = int(geom.envelope.area)
            z_l = 16
            for k_area, v_zoom in LANIS_ZOOM_LUT.items():
                if k_area < area:
                    z_l = v_zoom
                    break
            zoom_lvl = z_l
        except AttributeError:
            # If no geometry has been added, yet.
            x = 1
            y = 1
            zoom_lvl = 6
        return LANIS_LINK_TEMPLATE.format(
            zoom_lvl,
            x,
            y,
        )

    def get_documents(self) -> (QuerySet, QuerySet):
        """ Getter for all documents of an intervention

        Returns:
            revoc_docs (QuerySet): The queryset of a revocation document
            regular_docs (QuerySet): The queryset of regular other documents
        """
        revoc_docs = RevocationDocument.objects.filter(
            instance__in=self.legal.revocations.all()
        )
        regular_docs = InterventionDocument.objects.filter(
            instance=self
        )
        return revoc_docs, regular_docs

    def set_unchecked(self):
        super().set_unchecked()

    def set_checked(self, user: User) -> UserActionLogEntry:
        log_entry = super().set_checked(user)
        if log_entry is not None:
            self.add_log_entry_to_compensations(log_entry)
        return log_entry

    def set_unrecorded(self, user: User):
        log_entry = super().set_unrecorded(user)
        self.add_log_entry_to_compensations(log_entry)

    def set_recorded(self, user: User) -> UserActionLogEntry:
        log_entry = super().set_recorded(user)
        self.add_log_entry_to_compensations(log_entry)
        return log_entry

    def add_log_entry_to_compensations(self, log_entry: UserActionLogEntry):
        """ Adds the log entry to related compensations

        Args:
            log_entry (UserActionLogEntry): The log entry

        Returns:

        """
        comps = self.compensations.all()
        for comp in comps:
            comp.log.add(log_entry)

    def add_payment(self, form):
        """ Adds a new payment to the intervention

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

        Returns:

        """
        from compensation.models import Payment
        form_data = form.cleaned_data
        user = form.user
        with transaction.atomic():
            created_action = UserActionLogEntry.get_created_action(user)
            pay = Payment.objects.create(
                created=created_action,
                amount=form_data.get("amount", -1),
                due_on=form_data.get("due", None),
                comment=form_data.get("comment", None),
                intervention=self,
            )
        return pay

    def add_revocation(self, form):
        """ Adds a new revocation to the intervention

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

        Returns:

        """
        form_data = form.cleaned_data
        user = form.user
        with transaction.atomic():
            created_action = UserActionLogEntry.get_created_action(user)

            revocation = Revocation.objects.create(
                date=form_data["date"],
                legal=self.legal,
                comment=form_data["comment"],
                created=created_action,
            )

            if form_data["file"]:
                RevocationDocument.objects.create(
                    title="revocation_of_{}".format(self.identifier),
                    date_of_creation=form_data["date"],
                    comment=form_data["comment"],
                    file=form_data["file"],
                    instance=revocation
                )
        return revocation

    def add_deduction(self, form):
        """ Adds a new deduction to the intervention

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

        Returns:

        """
        form_data = form.cleaned_data
        user = form.user

        with transaction.atomic():
            user_action_create = UserActionLogEntry.get_created_action(user)
            deduction = EcoAccountDeduction.objects.create(
                intervention=self,
                account=form_data["account"],
                surface=form_data["surface"],
                created=user_action_create,
            )
        return deduction

    def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
        """ In case the object or a related object changed, internal processes need to be started, such as
        unrecord and uncheck

        Args:
            performing_user (User): The user which performed the editing action

        Returns:

        """
        user_action_edit = UserActionLogEntry.get_edited_action(performing_user, comment=edit_comment)
        self.log.add(user_action_edit)
        self.modified = user_action_edit
        self.save()

        super().mark_as_edited(performing_user, request)
        if self.checked:
            self.set_unchecked()

    def set_status_messages(self, request: HttpRequest):
        """ Setter for different information that need to be rendered

        Adds messages to the given HttpRequest

        Args:
            request (HttpRequest): The incoming request

        Returns:
            request (HttpRequest): The modified request
        """
        if not self.is_shared_with(request.user):
            messages.info(request, DATA_UNSHARED_EXPLANATION)
        request = self.set_geometry_conflict_message(request)
        return request


class InterventionDocument(AbstractDocument):
    """
    Specializes document upload for an intervention with certain path
    """
    instance = models.ForeignKey(
        Intervention,
        on_delete=models.CASCADE,
        related_name="documents",
    )
    file = models.FileField(
        upload_to=generate_document_file_upload_path,
        max_length=1000,
    )

    def delete(self, *args, **kwargs):
        """
        Custom delete functionality for InterventionDocuments.
        Removes the folder from the file system if there are no further documents for this entry.

        Args:
            *args ():
            **kwargs ():

        Returns:

        """
        revoc_docs, other_intervention_docs = self.instance.get_documents()

        folder_path = None
        if revoc_docs.count() == 0 and other_intervention_docs.count() == 1:
            # The only file left for this intervention is the one which is currently processed and will be deleted
            # Make sure that the intervention folder itself is deleted as well, not only the file
            # Therefore take the folder path from the file path
            folder_path = self.file.path.split("/")[:-1]
            folder_path = "/".join(folder_path)

        # Remove the file itself
        super().delete(*args, **kwargs)

        # If a folder path has been set, we need to delete the whole folder!
        if folder_path is not None:
            try:
                shutil.rmtree(folder_path)
            except FileNotFoundError:
                # Folder seems to be missing already...
                pass