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

"""
import json
from abc import abstractmethod

from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax
from django import forms
from django.contrib import messages
from django.contrib.gis import gdal
from django.db.models.fields.files import FieldFile

from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User
from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _

from konova.contexts import BaseContext
from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument
from konova.settings import DEFAULT_SRID
from konova.tasks import celery_update_parcels
from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
from user.models import UserActionLogEntry


class BaseForm(forms.Form):
    """
    Basic form for that holds attributes needed in all other forms
    """
    template = None
    action_url = None
    action_btn_label = _("Save")
    form_title = None
    cancel_redirect = None
    form_caption = None
    instance = None  # The data holding model object
    request = None
    form_attrs = {}  # Holds additional attributes, that can be used in the template
    has_required_fields = False  # Automatically set. Triggers hint rendering in templates
    show_cancel_btn = True

    def __init__(self, *args, **kwargs):
        self.instance = kwargs.pop("instance", None)
        super().__init__(*args, **kwargs)
        if self.request is not None:
            self.user = self.request.user
        # Check for required fields
        for _field_name, _field_val in self.fields.items():
            if _field_val.required:
                self.has_required_fields = True
                break

        self.check_for_recorded_instance()

    @abstractmethod
    def save(self):
        # To be implemented in subclasses!
        pass

    def disable_form_field(self, field: str):
        """
        Disables a form field for user editing
        """
        self.fields[field].widget.attrs["readonly"] = True
        self.fields[field].disabled = True
        self.fields[field].widget.attrs["title"] = _("Not editable")

    def initialize_form_field(self, field: str, val):
        """
        Initializes a form field with a value
        """
        self.fields[field].initial = val

    def add_placeholder_for_field(self, field: str, val):
        """
        Adds a placeholder to a field after initialization without the need to redefine the form widget

        Args:
            field (str): Field name
            val (str): Placeholder

        Returns:

        """
        self.fields[field].widget.attrs["placeholder"] = val

    def load_initial_data(self, form_data: dict, disabled_fields: list = None):
        """ Initializes form data from instance

        Inserts instance data into form and disables form fields

        Returns:

        """
        if self.instance is None:
            return
        for k, v in form_data.items():
            self.initialize_form_field(k, v)
        if disabled_fields:
            for field in disabled_fields:
                self.disable_form_field(field)

    def add_widget_html_class(self, field: str, cls: str):
        """ Adds a HTML class string to the widget of a field

        Args:
            field (str): The field's name
            cls (str): The new class string

        Returns:

        """
        set_class = self.fields[field].widget.attrs.get("class", "")
        if cls in set_class:
            return
        else:
            set_class += " " + cls
        self.fields[field].widget.attrs["class"] = set_class

    def remove_widget_html_class(self, field: str, cls: str):
        """ Removes a HTML class string from the widget of a field

        Args:
            field (str): The field's name
            cls (str): The new class string

        Returns:

        """
        set_class = self.fields[field].widget.attrs.get("class", "")
        set_class = set_class.replace(cls, "")
        self.fields[field].widget.attrs["class"] = set_class

    def check_for_recorded_instance(self):
        """ Checks if the instance is recorded and runs some special logic if yes

        If the instance is recorded, the form shall not display any possibility to
        edit any data. Instead, the users should get some information about why they can not edit anything.

        There are situations where the form should be rendered regularly,
        e.g deduction forms for (recorded) eco accounts.

        Returns:

        """
        from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
            RemoveEcoAccountDeductionModalForm
        is_none = self.instance is None
        is_other_data_type = not isinstance(self.instance, BaseObject)
        is_deduction_form = isinstance(
            self,
            (
                NewDeductionModalForm,
                EditEcoAccountDeductionModalForm,
                RemoveEcoAccountDeductionModalForm,
            )
        )

        if is_none or is_other_data_type or is_deduction_form:
            # Do nothing
            return

        if self.instance.is_recorded:
            self.template = "form/recorded_no_edit.html"


class RemoveForm(BaseForm):
    check = forms.BooleanField(
        label=_("Confirm"),
        label_suffix=_(""),
        required=True,
    )

    def __init__(self, *args, **kwargs):
        self.object_to_remove = kwargs.pop("object_to_remove", None)
        self.remove_post_url = kwargs.pop("remove_post_url", "")
        self.cancel_url = kwargs.pop("cancel_url", "")

        super().__init__(*args, **kwargs)

        self.form_title = _("Remove")
        if self.object_to_remove is not None:
            self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
        self.action_url = self.remove_post_url
        self.cancel_redirect = self.cancel_url

    def is_checked(self) -> bool:
        return self.cleaned_data.get("check", False)

    def save(self, user: User):
        """ Perform generic removing by running the form typical 'save()' method

        Args:
            user (User): The performing user

        Returns:

        """
        if self.object_to_remove is not None and self.is_checked():
            with transaction.atomic():
                self.object_to_remove.is_active = False
                action = UserActionLogEntry.get_deleted_action(user)
                self.object_to_remove.deleted = action
                self.object_to_remove.save()
        return self.object_to_remove


class BaseModalForm(BaseForm, BSModalForm):
    """ A specialzed form class for modal form handling

    """
    is_modal_form = True
    render_submit = True
    template = "modal/modal_form.html"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.action_btn_label = _("Continue")

    def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
        """ Generic processing of request

        Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used

        Args:
            request (HttpRequest): The incoming request
            msg_success (str): The message in case of successful removing
            msg_error (str): The message in case of an error

        Returns:

        """
        redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
        template = self.template
        if request.method == "POST":
            if self.is_valid():
                if not is_ajax(request.META):
                    # Modal forms send one POST for checking on data validity. This can be used to return possible errors
                    # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
                    # saving/commiting of the data to the database. is_ajax() performs this check. The first request is
                    # an ajax call, the second is a regular form POST.
                    self.save()
                    messages.success(
                        request,
                        msg_success
                    )
                return HttpResponseRedirect(redirect_url)
            else:
                context = {
                    "form": self,
                }
                context = BaseContext(request, context).context
                return render(request, template, context)
        elif request.method == "GET":
            context = {
                "form": self,
            }
            context = BaseContext(request, context).context
            return render(request, template, context)
        else:
            raise NotImplementedError


class SimpleGeomForm(BaseForm):
    """ A geometry form for rendering geometry read-only using a widget

    """
    read_only = True
    geom = MultiPolygonField(
        srid=DEFAULT_SRID_RLP,
        label=_("Geometry"),
        help_text=_(""),
        label_suffix="",
        required=False,
        disabled=False,
    )

    def __init__(self, *args, **kwargs):
        self.read_only = kwargs.pop("read_only", True)
        super().__init__(*args, **kwargs)

        # Initialize geometry
        try:
            geom = self.instance.geometry.geom
            self.empty = geom.empty

            if self.empty:
                raise AttributeError

            geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
            geom = json.dumps(geojson)
        except AttributeError:
            # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
            geom = ""
            self.empty = True

        self.initialize_form_field("geom", geom)

    def is_valid(self):
        super().is_valid()
        is_valid = True

        # Get geojson from form
        geom = self.data["geom"]
        if geom is None or len(geom) == 0:
            # empty geometry is a valid geometry
            return is_valid
        geom = json.loads(geom)

        # Write submitted data back into form field to make sure invalid geometry
        # will be rendered again on failed submit
        self.initialize_form_field("geom", self.data["geom"])

        # Read geojson into gdal geometry
        # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
        # this case)
        features = []
        features_json = geom.get("features", [])
        for feature in features_json:
            g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
            if g.geom_type not in ["Polygon", "MultiPolygon"]:
                self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
                is_valid = False
                return is_valid

            polygon = Polygon.from_ewkt(g.ewkt)
            is_valid = polygon.valid
            if not is_valid:
                self.add_error("geom", polygon.valid_reason)
                return is_valid

            features.append(polygon)
        form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
        for feature in features:
            form_geom = form_geom.union(feature)

        # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
        if form_geom.geom_type != "MultiPolygon":
            form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)

        # Write unioned Multipolygon into cleaned data
        if self.cleaned_data is None:
            self.cleaned_data = {}
        self.cleaned_data["geom"] = form_geom.ewkt

        return is_valid

    def save(self, action: UserActionLogEntry):
        """ Saves the form's geometry

        Creates a new geometry entry if none is set, yet

        Args:
            action ():

        Returns:

        """
        try:
            if self.instance is None or self.instance.geometry is None:
                raise LookupError
            geometry = self.instance.geometry
            geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
            geometry.modified = action

            geometry.save()
        except LookupError:
            # No geometry or linked instance holding a geometry exist --> create a new one!
            geometry = Geometry.objects.create(
                geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
                created=action,
            )
        # Start the parcel update procedure in a background process
        celery_update_parcels.delay(geometry.id)
        return geometry


class RemoveModalForm(BaseModalForm):
    """ Generic removing modal form

    Can be used for anything, where removing shall be confirmed by the user a second time.

    """
    confirm = forms.BooleanField(
        label=_("Confirm"),
        label_suffix=_(""),
        widget=forms.CheckboxInput(),
        required=True,
    )

    def __init__(self, *args, **kwargs):
        self.template = "modal/modal_form.html"
        super().__init__(*args, **kwargs)
        self.form_title = _("Remove")
        self.form_caption = _("Are you sure?")
        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
        self.fields["confirm"].widget.attrs["class"] = ""

    def save(self):
        if isinstance(self.instance, BaseObject):
            self.instance.mark_as_deleted(self.user)
        else:
            # If the class does not provide restorable delete functionality, we must delete the entry finally
            self.instance.delete()


class RemoveDeadlineModalForm(RemoveModalForm):
    """ Removing modal form for deadlines

    Can be used for anything, where removing shall be confirmed by the user a second time.

    """
    deadline = None

    def __init__(self, *args, **kwargs):
        deadline = kwargs.pop("deadline", None)
        self.deadline = deadline
        super().__init__(*args, **kwargs)

    def save(self):
        self.instance.remove_deadline(self)


class NewDocumentModalForm(BaseModalForm):
    """ Modal form for new documents

    """
    title = forms.CharField(
        label=_("Title"),
        label_suffix=_(""),
        max_length=500,
        widget=forms.TextInput(
            attrs={
                "class": "form-control",
            }
        )
    )
    creation_date = forms.DateField(
        label=_("Created on"),
        label_suffix=_(""),
        help_text=_("When has this file been created? Important for photos."),
        widget=forms.DateInput(
            attrs={
                "type": "date",
                "data-provide": "datepicker",
                "class": "form-control",
            },
            format="%d.%m.%Y"
        )
    )
    file = forms.FileField(
        label=_("File"),
        label_suffix=_(""),
        help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
        widget=forms.FileInput(
            attrs={
                "class": "form-control-file",
            }
        ),
    )
    comment = forms.CharField(
        required=False,
        max_length=200,
        label=_("Comment"),
        label_suffix=_(""),
        help_text=_("Additional comment, maximum {} letters").format(200),
        widget=forms.Textarea(
            attrs={
                "cols": 30,
                "rows": 5,
                "class": "form-control",
            }
        )
    )
    document_model = None

    class Meta:
        abstract = True

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.form_title = _("Add new document")
        self.form_caption = _("")
        self.form_attrs = {
            "enctype": "multipart/form-data",  # important for file upload
        }
        if not self.document_model:
            raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))

    def is_valid(self):
        super_valid = super().is_valid()

        _file = self.cleaned_data.get("file", None)

        if _file is None or isinstance(_file, FieldFile):
            # FieldFile declares that no new file has been uploaded and we do not need to check on the file again
            return super_valid

        mime_type_valid = self.document_model.is_mime_type_valid(_file)
        if not mime_type_valid:
            self.add_error(
                "file",
                FILE_TYPE_UNSUPPORTED
            )

        file_size_valid = self.document_model.is_file_size_valid(_file)
        if not file_size_valid:
            self.add_error(
                "file",
                FILE_SIZE_TOO_LARGE
            )

        file_valid = mime_type_valid and file_size_valid
        return super_valid and file_valid

    def save(self):
        with transaction.atomic():
            action = UserActionLogEntry.get_created_action(self.user)
            edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))

            doc = self.document_model.objects.create(
                created=action,
                title=self.cleaned_data["title"],
                comment=self.cleaned_data["comment"],
                file=self.cleaned_data["file"],
                date_of_creation=self.cleaned_data["creation_date"],
                instance=self.instance,
            )

            self.instance.log.add(edited_action)
            self.instance.modified = edited_action
            self.instance.save()

            return doc


class EditDocumentModalForm(NewDocumentModalForm):
    document = None
    document_model = AbstractDocument

    def __init__(self, *args, **kwargs):
        self.document = kwargs.pop("document", None)
        super().__init__(*args, **kwargs)
        self.form_title = _("Edit document")
        form_data = {
            "title": self.document.title,
            "comment": self.document.comment,
            "creation_date": str(self.document.date_of_creation),
            "file": self.document.file,
        }
        self.load_initial_data(form_data)


    def save(self):
        with transaction.atomic():
            document = self.document
            file = self.cleaned_data.get("file", None)

            document.title = self.cleaned_data.get("title", None)
            document.comment = self.cleaned_data.get("comment", None)
            document.date_of_creation = self.cleaned_data.get("creation_date", None)
            if not isinstance(file, FieldFile):
                document.replace_file(file)
            document.save()

            self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)

            return document


class RecordModalForm(BaseModalForm):
    """ Modal form for recording data

    """
    confirm = forms.BooleanField(
        label=_("Confirm record"),
        label_suffix="",
        widget=forms.CheckboxInput(),
        required=True,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.form_title = _("Record data")
        self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
        # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
        self.fields["confirm"].widget.attrs["class"] = ""

        if self.instance.recorded:
            # unrecord!
            self.fields["confirm"].label = _("Confirm unrecord")
            self.form_title = _("Unrecord data")
            self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)

        if not isinstance(self.instance, RecordableObjectMixin):
            raise NotImplementedError

    def is_valid(self):
        """ Checks for instance's validity and data quality

        Returns:

        """
        from intervention.models import Intervention
        super_val = super().is_valid()
        if self.instance.recorded:
            # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
            return super_val
        checker = self.instance.quality_check()
        for msg in checker.messages:
            self.add_error(
                "confirm",
                msg
            )
        valid = checker.valid
        # Special case: Intervention
        # Add direct checks for related compensations
        if isinstance(self.instance, Intervention):
            comps_valid = self._are_compensations_valid()
            valid = valid and comps_valid
        return super_val and valid

    def _are_deductions_valid(self):
        """ Performs validity checks on deductions and their eco-account

        Returns:

        """
        deductions = self.instance.deductions.all()
        for deduction in deductions:
            checker = deduction.account.quality_check()
            for msg in checker.messages:
                self.add_error(
                    "confirm",
                    f"{deduction.account.identifier}: {msg}"
                )
            return checker.valid
        return True

    def _are_compensations_valid(self):
        """ Runs a special case for intervention-compensations validity

        Returns:

        """
        comps = self.instance.compensations.filter(
            deleted=None,
        )
        comps_valid = True
        for comp in comps:
            checker = comp.quality_check()
            comps_valid = comps_valid and checker.valid
            for msg in checker.messages:
                self.add_error(
                    "confirm",
                    f"{comp.identifier}: {msg}"
                )

        deductions_valid = self._are_deductions_valid()

        return comps_valid and deductions_valid

    def save(self):
        with transaction.atomic():
            if self.cleaned_data["confirm"]:
                if self.instance.recorded:
                    self.instance.set_unrecorded(self.user)
                else:
                    self.instance.set_recorded(self.user)
        return self.instance

    def check_for_recorded_instance(self):
        """ Overwrite the check method for doing nothing on the RecordModalForm

        Returns:

        """
        pass
