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

"""
import shutil

from django.contrib.auth.models import User
from django.contrib.gis.db import models
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _

from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, \
    CODELIST_COMPENSATION_FUNDING_ID
from compensation.managers import CompensationStateManager, EcoAccountDeductionManager, CompensationActionManager, \
    EcoAccountManager, CompensationManager
from intervention.models import Intervention, ResponsibilityData
from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \
    generate_document_file_upload_path
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from user.models import UserActionLogEntry


class Payment(BaseResource):
    """
    Holds data on a payment for an intervention (alternative to a classic compensation)
    """
    amount = models.FloatField(validators=[MinValueValidator(limit_value=0.00)])
    due_on = models.DateField(null=True, blank=True)
    comment = models.CharField(
        max_length=1000,
        null=True,
        blank=True,
        help_text="Refers to german money transfer 'Verwendungszweck'",
    )
    intervention = models.ForeignKey(
        Intervention,
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name='payments'
    )

    class Meta:
        ordering = [
            "-amount",
        ]


class CompensationState(UuidModel):
    """
    Compensations must define the state of an area before and after the compensation.
    """
    biotope_type = models.ForeignKey(
        KonovaCode,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        limit_choices_to={
            "code_lists__in": [CODELIST_BIOTOPES_ID],
            "is_selectable": True,
            "is_archived": False,
        }
    )
    surface = models.FloatField()

    objects = CompensationStateManager()

    def __str__(self):
        return "{} | {} m²".format(self.biotope_type, self.surface)


class UnitChoices(models.TextChoices):
    """
    Predefines units for selection
    """
    cm = "cm", _("cm")
    m = "m", _("m")
    km = "km", _("km")
    qm = "qm", _("m²")
    ha = "ha", _("ha")
    st = "pcs", _("Pieces")  # pieces


class CompensationAction(BaseResource):
    """
    Compensations include actions like planting trees, refreshing rivers and so on.
    """
    action_type = models.ForeignKey(
        KonovaCode,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        limit_choices_to={
            "code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
            "is_selectable": True,
            "is_archived": False,
        }
    )
    amount = models.FloatField()
    unit = models.CharField(max_length=100, null=True, blank=True, choices=UnitChoices.choices)
    comment = models.TextField(blank=True, null=True, help_text="Additional comment")

    objects = CompensationActionManager()

    def __str__(self):
        return "{} | {} {}".format(self.action_type, self.amount, self.unit)

    @property
    def unit_humanize(self):
        """ Returns humanized version of enum

        Used for template rendering

        Returns:

        """
        choices = UnitChoices.choices
        for choice in choices:
            if choice[0] == self.unit:
                return choice[1]
        return None


class AbstractCompensation(BaseObject):
    """
    Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
    EMA or EcoAccount.

    """
    responsible = models.OneToOneField(
        ResponsibilityData,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle') and handler",
    )

    before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'")
    after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'")
    actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'")

    fundings = models.ManyToManyField(
        KonovaCode,
        blank=True,
        limit_choices_to={
            "code_lists__in": [CODELIST_COMPENSATION_FUNDING_ID],
            "is_selectable": True,
            "is_archived": False,
        },
        help_text="List of funding project codes",
    )

    deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+")

    geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)

    class Meta:
        abstract = True

    def get_surface(self) -> float:
        """ Calculates the compensation's/account's surface

        Returns:
            sum_surface (float)
        """
        return self.after_states.all().aggregate(Sum("surface"))["surface__sum"]


class Compensation(AbstractCompensation):
    """
    Regular compensation, linked to an intervention
    """
    intervention = models.ForeignKey(
        Intervention,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='compensations'
    )

    objects = CompensationManager()

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

    def save(self, *args, **kwargs):
        if self.identifier is None or len(self.identifier) == 0:
            # Create new identifier
            new_id = self.generate_new_identifier()
            while Compensation.objects.filter(identifier=new_id).exists():
                new_id = self.generate_new_identifier()
            self.identifier = new_id
        super().save(*args, **kwargs)

    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
            zoom_lvl = 16
        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:
        """ Getter for all documents of a compensation

        Returns:
            docs (QuerySet): The queryset of all documents
        """
        docs = CompensationDocument.objects.filter(
            instance=self
        )
        return docs


class CompensationDocument(AbstractDocument):
    """
    Specializes document upload for revocations with certain path
    """
    instance = models.ForeignKey(
        Compensation,
        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 CompensationDocuments.
        Removes the folder from the file system if there are no further documents for this entry.

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

        Returns:

        """
        comp_docs = self.instance.get_documents()

        folder_path = None
        if comp_docs.count() == 1:
            # The only file left for this compensation is the one which is currently processed and will be deleted
            # Make sure that the compensation 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


class EcoAccount(AbstractCompensation):
    """
    An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
    with some kind of currency. From this account one is able to deduct currency for current projects.
    """
    # Users having access on this object
    # Not needed in regular Compensation since their access is defined by the linked intervention's access
    users = models.ManyToManyField(
        User,
        help_text="Users having access (shared with)"
    )

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

    deductable_surface = models.FloatField(
        blank=True,
        null=True,
        help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
        default=0,
    )

    objects = EcoAccountManager()

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

    def clean(self):
        # Deductable surface can not be larger than added states after surface
        after_state_sum = self.get_state_after_surface_sum()
        if self.deductable_surface > after_state_sum:
            raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))

        # Deductable surface can not be lower than amount of already deducted surfaces
        # User needs to contact deducting user in case of further problems
        deducted_sum = self.get_deductions_surface()
        if self.deductable_surface < deducted_sum:
            raise ValidationError(
                _("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
            )

    def save(self, *args, **kwargs):
        if self.identifier is None or len(self.identifier) == 0:
            # Create new identifier
            new_id = self.generate_new_identifier()
            while EcoAccount.objects.filter(identifier=new_id).exists():
                new_id = self.generate_new_identifier()
            self.identifier = new_id
        super().save(*args, **kwargs)

    @property
    def deductions_surface_sum(self) -> float:
        """ Shortcut for get_deductions_surface.

        Can be used in templates

        Returns:
            sum_surface (float)
        """
        return self.get_deductions_surface()

    def get_deductions_surface(self) -> float:
        """ Calculates the account's deductions surface sum

        Returns:
            sum_surface (float)
        """
        return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0

    def get_state_after_surface_sum(self) -> float:
        """ Calculates the account's after state surface sum

        Returns:
            sum_surface (float)
        """
        return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0

    def get_available_rest(self) -> (float, float):
        """ Calculates available rest surface of the eco account

        Args:

        Returns:
            ret_val_total (float): Total amount
            ret_val_relative (float): Amount as percentage (0-100)
        """
        deductions = self.deductions.filter(
            intervention__deleted=None,
        )
        deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
        available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
        ret_val_total = available_surfaces - deductions_surfaces

        if available_surfaces > 0:
            ret_val_relative = int((ret_val_total / available_surfaces) * 100)
        else:
            ret_val_relative = 0

        return ret_val_total, ret_val_relative

    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
            zoom_lvl = 16
        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 quality_check(self) -> list:
        """ Quality check

        Returns:
            ret_msgs (list): Holds error messages
        """
        ret_msgs = []

        # ToDo: Add check methods!

        return ret_msgs

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

        Returns:
            docs (QuerySet): The queryset of all documents
        """
        docs = EcoAccountDocument.objects.filter(
            instance=self
        )
        return docs


class EcoAccountDocument(AbstractDocument):
    """
    Specializes document upload for revocations with certain path
    """
    instance = models.ForeignKey(
        EcoAccount,
        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 EcoAccountDocuments.
        Removes the folder from the file system if there are no further documents for this entry.

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

        Returns:

        """
        acc_docs = self.instance.get_documents()

        folder_path = None
        if acc_docs.count() == 1:
            # The only file left for this eco account is the one which is currently processed and will be deleted
            # Make sure that the compensation 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


class EcoAccountDeduction(BaseResource):
    """
    A deduction object for eco accounts
    """
    account = models.ForeignKey(
        EcoAccount,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        help_text="Deducted from",
        related_name="deductions",
    )
    surface = models.FloatField(
        null=True,
        blank=True,
        help_text="Amount deducted (m²)",
        validators=[
            MinValueValidator(limit_value=0.00),
        ]
    )
    intervention = models.ForeignKey(
        Intervention,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="Deducted for",
        related_name="deductions",
    )

    objects = EcoAccountDeductionManager()

    def __str__(self):
        return "{} of {}".format(self.surface, self.account)