"""
django-helpdesk - A Django powered ticket tracker for small enterprise.

(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.

forms.py - Definitions of newforms-based forms for creating and maintaining
           tickets.
"""

from datetime import datetime
from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from helpdesk import settings as helpdesk_settings
from helpdesk.lib import (
    convert_value,
    process_attachments,
    safe_template_context,
    get_assignable_users,
)
from helpdesk.models import (
    Checklist,
    ChecklistTemplate,
    CustomField,
    FollowUp,
    IgnoreEmail,
    Queue,
    Ticket,
    TicketCC,
    TicketCustomFieldValue,
    TicketDependency,
    UserSettings,
)
from helpdesk.settings import (
    CUSTOMFIELD_DATE_FORMAT,
    CUSTOMFIELD_DATETIME_FORMAT,
    CUSTOMFIELD_TIME_FORMAT,
    CUSTOMFIELD_TO_FIELD_DICT,
    HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST,
)
from helpdesk.validators import validate_file_extension
from helpdesk.signals import new_ticket_done
import logging


if helpdesk_settings.HELPDESK_KB_ENABLED:
    from helpdesk.models import KBItem

logger = logging.getLogger(__name__)
User = get_user_model()


class CustomFieldMixin(object):
    """
    Mixin that provides a method to turn CustomFields into an actual field
    """

    def customfield_to_field(self, field, instanceargs):
        # Use TextInput widget by default
        instanceargs["widget"] = forms.TextInput(attrs={"class": "form-control"})
        # if-elif branches start with special cases
        if field.data_type == "varchar":
            fieldclass = forms.CharField
            instanceargs["max_length"] = field.max_length
        elif field.data_type == "text":
            fieldclass = forms.CharField
            instanceargs["widget"] = forms.Textarea(attrs={"class": "form-control"})
            instanceargs["max_length"] = field.max_length
        elif field.data_type == "integer":
            fieldclass = forms.IntegerField
            instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
        elif field.data_type == "decimal":
            fieldclass = forms.DecimalField
            instanceargs["decimal_places"] = field.decimal_places
            instanceargs["max_digits"] = field.max_length
            instanceargs["widget"] = forms.NumberInput(attrs={"class": "form-control"})
        elif field.data_type == "list":
            fieldclass = forms.ChoiceField
            instanceargs["choices"] = field.get_choices()
            instanceargs["widget"] = forms.Select(attrs={"class": "form-control"})
        else:
            # Try to use the immediate equivalences dictionary
            try:
                fieldclass = CUSTOMFIELD_TO_FIELD_DICT[field.data_type]
                # Change widgets for the following classes
                if fieldclass == forms.DateField:
                    instanceargs["widget"] = forms.DateInput(
                        attrs={"class": "form-control date-field"}
                    )
                elif fieldclass == forms.DateTimeField:
                    instanceargs["widget"] = forms.DateTimeInput(
                        attrs={"class": "form-control datetime-field"}
                    )
                elif fieldclass == forms.TimeField:
                    instanceargs["widget"] = forms.TimeInput(
                        attrs={"class": "form-control time-field"}
                    )
                elif fieldclass == forms.BooleanField:
                    instanceargs["widget"] = forms.CheckboxInput(
                        attrs={"class": "form-control"}
                    )

            except KeyError:
                # The data_type was not found anywhere
                raise NameError("Unrecognized data_type %s" % field.data_type)

        self.fields["custom_%s" % field.name] = fieldclass(**instanceargs)


class EditTicketForm(CustomFieldMixin, forms.ModelForm):
    class Meta:
        model = Ticket
        exclude = (
            "created",
            "modified",
            "status",
            "on_hold",
            "resolution",
            "last_escalation",
            "assigned_to",
        )

    class Media:
        js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")

    def __init__(self, *args, **kwargs):
        """
        Add any custom fields that are defined to the form
        """
        super(EditTicketForm, self).__init__(*args, **kwargs)

        # Since title is max 100 characters limit it in editing
        if "title" in self.fields:
            self.fields["title"].max_length = Ticket._meta.get_field("title").max_length
            self.fields["title"].widget.attrs["maxlength"] = Ticket._meta.get_field(
                "title"
            ).max_length

        # Disable and add help_text to the merged_to field on this form
        self.fields["merged_to"].disabled = True
        self.fields["merged_to"].help_text = _(
            "This ticket is merged into the selected ticket."
        )

        for field in CustomField.objects.all():
            initial_value = None
            try:
                current_value = TicketCustomFieldValue.objects.get(
                    ticket=self.instance, field=field
                )
                initial_value = current_value.value
                # Attempt to convert from fixed format string to date/time data
                # type
                if "datetime" == current_value.field.data_type:
                    initial_value = datetime.strptime(
                        initial_value, CUSTOMFIELD_DATETIME_FORMAT
                    )
                elif "date" == current_value.field.data_type:
                    initial_value = datetime.strptime(
                        initial_value, CUSTOMFIELD_DATE_FORMAT
                    )
                elif "time" == current_value.field.data_type:
                    initial_value = datetime.strptime(
                        initial_value, CUSTOMFIELD_TIME_FORMAT
                    )
                # If it is boolean field, transform the value to a real boolean
                # instead of a string
                elif "boolean" == current_value.field.data_type:
                    initial_value = "True" == initial_value
            except (TicketCustomFieldValue.DoesNotExist, ValueError, TypeError):
                # ValueError error if parsing fails, using initial_value = current_value.value
                # TypeError if parsing None type
                pass
            instanceargs = {
                "label": field.label,
                "help_text": field.help_text,
                "required": field.required,
                "initial": initial_value,
            }

            self.customfield_to_field(field, instanceargs)

    def save(self, *args, **kwargs):
        for field, value in self.cleaned_data.items():
            if field.startswith("custom_"):
                field_name = field.replace("custom_", "", 1)
                customfield = CustomField.objects.get(name=field_name)
                try:
                    cfv = TicketCustomFieldValue.objects.get(
                        ticket=self.instance, field=customfield
                    )
                except ObjectDoesNotExist:
                    cfv = TicketCustomFieldValue(
                        ticket=self.instance, field=customfield
                    )

                cfv.value = convert_value(value)
                cfv.save()

        return super(EditTicketForm, self).save(*args, **kwargs)


class EditTicketCustomFieldForm(EditTicketForm):
    """
    Uses the EditTicketForm logic to provide a form for Ticket custom fields.
    """

    def __init__(self, *args, **kwargs):
        """
        Add any custom fields that are defined to the form
        """
        super(EditTicketCustomFieldForm, self).__init__(*args, **kwargs)

        if HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST:
            fields = list(self.fields)
            for field in fields:
                if (
                    field != "id"
                    and field.replace("custom_", "", 1)
                    not in HELPDESK_SHOW_CUSTOM_FIELDS_FOLLOW_UP_LIST
                ):
                    self.fields.pop(field, None)

    def save(self, *args, **kwargs):
        # if form is saved in a ticket update, it is passed
        # a followup instance to trace custom fields changes
        if "followup" in kwargs:
            followup = kwargs.pop("followup", None)

            for field, value in self.cleaned_data.items():
                if field.startswith("custom_"):
                    if value != self.fields[field].initial:
                        followup.ticketchange_set.create(
                            field=field.replace("custom_", "", 1),
                            old_value=self.fields[field].initial,
                            new_value=value,
                        )

        super(EditTicketCustomFieldForm, self).save(*args, **kwargs)

    class Meta:
        model = Ticket
        fields = (
            "id",
            "merged_to",
        )


class EditFollowUpForm(forms.ModelForm):
    class Meta:
        model = FollowUp
        exclude = (
            "date",
            "user",
        )

    def __init__(self, *args, **kwargs):
        """Filter not opened tickets here."""
        super(EditFollowUpForm, self).__init__(*args, **kwargs)
        self.fields["ticket"].queryset = Ticket.objects.filter(
            status__in=Ticket.OPEN_STATUSES
        )


class AbstractTicketForm(CustomFieldMixin, forms.Form):
    """
    Contain all the common code and fields between "TicketForm" and
    "PublicTicketForm". This Form is not intended to be used directly.
    """

    queue = forms.ChoiceField(
        widget=forms.Select(attrs={"class": "form-control"}),
        label=_("Queue"),
        required=True,
        choices=(),
    )

    title = forms.CharField(
        max_length=200,
        required=True,
        widget=forms.TextInput(attrs={"class": "form-control"}),
        label=_("Summary of the problem"),
    )

    body = forms.CharField(
        widget=forms.Textarea(attrs={"class": "form-control"}),
        label=_("Description of your issue"),
        required=True,
        help_text=_("Please be as descriptive as possible and include all details"),
    )

    priority = forms.ChoiceField(
        widget=forms.Select(attrs={"class": "form-control"}),
        choices=Ticket.PRIORITY_CHOICES,
        required=True,
        initial=getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3"),
        label=_("Priority"),
        help_text=_("Please select a priority carefully. If unsure, leave it as '3'."),
    )

    due_date = forms.DateTimeField(
        widget=forms.TextInput(attrs={"class": "form-control", "autocomplete": "off"}),
        required=False,
        input_formats=[
            CUSTOMFIELD_DATE_FORMAT,
            CUSTOMFIELD_DATETIME_FORMAT,
            "%d/%m/%Y",
            "%m/%d/%Y",
            "%d.%m.%Y",
        ],
        label=_("Due on"),
    )

    if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
        attachment = forms.FileField(
            widget=forms.FileInput(attrs={"class": "form-control-file"}),
            required=False,
            label=_("Attach File"),
            help_text=_(
                "You can attach a file to this ticket. "
                "Only file types such as plain text (.txt), "
                "a document (.pdf, .docx, or .odt), "
                "or screenshot (.png or .jpg) may be uploaded."
            ),
            validators=[validate_file_extension],
        )

    class Media:
        js = ("helpdesk/js/init_due_date.js", "helpdesk/js/init_datetime_classes.js")

    def __init__(self, kbcategory=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if helpdesk_settings.HELPDESK_KB_ENABLED:
            if kbcategory:
                self.fields["kbitem"] = forms.ChoiceField(
                    widget=forms.Select(attrs={"class": "form-control"}),
                    required=False,
                    label=_("Knowledge Base Item"),
                    choices=[
                        (kbi.pk, kbi.title)
                        for kbi in KBItem.objects.filter(
                            category=kbcategory.pk, enabled=True
                        )
                    ],
                )

    def _add_form_custom_fields(self, staff_only_filter=None):
        if staff_only_filter is None:
            queryset = CustomField.objects.all()
        else:
            queryset = CustomField.objects.filter(staff_only=staff_only_filter)

        for field in queryset:
            instanceargs = {
                "label": field.label,
                "help_text": field.help_text,
                "required": field.required,
            }

            self.customfield_to_field(field, instanceargs)

    def _get_queue(self):
        # this procedure is re-defined for public submission form
        return Queue.objects.get(id=int(self.cleaned_data["queue"]))

    def _create_ticket(self):
        queue = self._get_queue()
        kbitem = None
        if "kbitem" in self.cleaned_data:
            kbitem = KBItem.objects.get(id=int(self.cleaned_data["kbitem"]))

        ticket = Ticket(
            title=self.cleaned_data["title"],
            submitter_email=self.cleaned_data["submitter_email"],
            created=timezone.now(),
            status=Ticket.OPEN_STATUS,
            queue=queue,
            description=self.cleaned_data["body"],
            priority=self.cleaned_data.get(
                "priority", getattr(settings, "HELPDESK_PUBLIC_TICKET_PRIORITY", "3")
            ),
            due_date=self.cleaned_data.get(
                "due_date", getattr(settings, "HELPDESK_PUBLIC_TICKET_DUE_DATE", None)
            )
            or None,
            kbitem=kbitem,
        )

        return ticket, queue

    def _create_custom_fields(self, ticket):
        ticket.save_custom_field_values(self.cleaned_data)

    def _create_follow_up(self, ticket, title, user=None):
        followup = FollowUp(
            ticket=ticket,
            title=title,
            date=timezone.now(),
            public=True,
            comment=self.cleaned_data["body"],
        )
        if user:
            followup.user = user
        return followup

    def _attach_files_to_follow_up(self, followup):
        files = self.cleaned_data.get("attachment")
        if files:
            files = process_attachments(followup, [files])
        return files

    @staticmethod
    def _send_messages(ticket, queue, followup, files, user=None):
        context = safe_template_context(ticket)
        context["comment"] = followup.comment

        roles = {
            "submitter": ("newticket_submitter", context),
            "new_ticket_cc": ("newticket_cc", context),
            "ticket_cc": ("newticket_cc", context),
        }
        if (
            ticket.assigned_to
            and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
        ):
            roles["assigned_to"] = ("assigned_owner", context)
        ticket.send(
            roles,
            fail_silently=True,
            files=files,
        )


class TicketForm(AbstractTicketForm):
    """
    Ticket Form for registered users.
    """

    submitter_email = forms.EmailField(
        required=False,
        label=_("Submitter E-Mail Address"),
        widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
        help_text=_(
            "This e-mail address will receive copies of all public "
            "updates to this ticket."
        ),
    )
    assigned_to = forms.ChoiceField(
        widget=(
            forms.Select(attrs={"class": "form-control"})
            if not helpdesk_settings.HELPDESK_CREATE_TICKET_HIDE_ASSIGNED_TO
            else forms.HiddenInput()
        ),
        required=False,
        label=_("Case owner"),
        help_text=_(
            "If you select an owner other than yourself, they'll be "
            "e-mailed details of this ticket immediately."
        ),
        choices=(),
    )

    def __init__(
        self, instance=None, queue_choices=None, body_reqd=True, *args, **kwargs
    ):
        """
        Add any custom fields that are defined to the form.
        The view will have injected extra kwargs into the form init
        by calling the views get_form_kwargs() which must be removed before
        calling super() because the django.forms.forms.BaseForm only
        supports specific kwargs and so will crash and burn if they are left in
        """
        super().__init__(*args, **kwargs)
        if queue_choices:
            self.fields["queue"].choices = queue_choices
        self.fields["body"].required = body_reqd
        self.fields["assigned_to"].choices = [("", "--------")] + [
            (u.id, u.get_username())
            for u in get_assignable_users(
                helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_OWNERS
            )
        ]
        self._add_form_custom_fields()

    def save(self, user):
        """
        Writes and returns a Ticket() object
        """

        ticket, queue = self._create_ticket()
        if self.cleaned_data["assigned_to"]:
            try:
                u = User.objects.get(id=self.cleaned_data["assigned_to"])
                ticket.assigned_to = u
            except User.DoesNotExist:
                ticket.assigned_to = None
        ticket.save()

        self._create_custom_fields(ticket)

        if self.cleaned_data["assigned_to"]:
            title = _("Ticket Opened & Assigned to %(name)s") % {
                "name": ticket.get_assigned_to or _("<invalid user>")
            }
        else:
            title = _("Ticket Opened")
        followup = self._create_follow_up(ticket, title=title, user=user)
        followup.save()

        if helpdesk_settings.HELPDESK_ENABLE_ATTACHMENTS:
            files = self._attach_files_to_follow_up(followup)
        else:
            files = None

        # emit signal when the TicketForm.save is done
        new_ticket_done.send(sender="TicketForm", ticket=ticket)

        self._send_messages(
            ticket=ticket, queue=queue, followup=followup, files=files, user=user
        )
        return ticket


class PublicTicketForm(AbstractTicketForm):
    """
    Ticket Form creation for all users (public-facing).
    """

    submitter_email = forms.EmailField(
        widget=forms.TextInput(attrs={"class": "form-control", "type": "email"}),
        required=True,
        label=_("Your E-Mail Address"),
        help_text=_("We will e-mail you when your ticket is updated."),
    )

    def __init__(self, hidden_fields=(), readonly_fields=(), *args, **kwargs):
        """
        Add any (non-staff) custom fields that are defined to the form
        """
        super(PublicTicketForm, self).__init__(*args, **kwargs)
        self._add_form_custom_fields(False)

        for field in self.fields.keys():
            if field in hidden_fields:
                self.fields[field].widget = forms.HiddenInput()
            if field in readonly_fields:
                self.fields[field].disabled = True

        field_deletion_table = {
            "queue": "HELPDESK_PUBLIC_TICKET_QUEUE",
            "priority": "HELPDESK_PUBLIC_TICKET_PRIORITY",
            "due_date": "HELPDESK_PUBLIC_TICKET_DUE_DATE",
        }

        for field_name, field_setting_key in field_deletion_table.items():
            has_settings_default_value = getattr(settings, field_setting_key, None)
            if has_settings_default_value is not None or (
                "HELPDESK_PUBLIC_TICKET_DUE_DATE" == field_setting_key
                and hasattr(settings, field_setting_key)
            ):
                del self.fields[field_name]

        public_queues = Queue.objects.filter(allow_public_submission=True)

        if len(public_queues) == 0:
            logger.warning(
                "There are no public queues defined - public ticket creation is impossible"
            )

        if "queue" in self.fields:
            self.fields["queue"].choices = [("", "--------")] + [
                (q.id, q.title) for q in public_queues
            ]

    def _get_queue(self):
        if getattr(settings, "HELPDESK_PUBLIC_TICKET_QUEUE", None) is not None:
            # force queue to be the pre-defined one
            # (only for public submissions)
            public_queue = Queue.objects.filter(
                slug=settings.HELPDESK_PUBLIC_TICKET_QUEUE
            ).first()
            if not public_queue:
                logger.fatal(
                    "Public queue '%s' is configured as default but can't be found",
                    settings.HELPDESK_PUBLIC_TICKET_QUEUE,
                )
            return public_queue
        else:
            # get the queue user entered
            return Queue.objects.get(id=int(self.cleaned_data["queue"]))

    def save(self, user):
        """
        Writes and returns a Ticket() object
        """
        ticket, queue = self._create_ticket()
        if queue.default_owner and not ticket.assigned_to:
            ticket.assigned_to = queue.default_owner
        ticket.save()

        self._create_custom_fields(ticket)

        followup = self._create_follow_up(
            ticket, title=_("Ticket Opened Via Web"), user=user
        )
        followup.save()

        files = self._attach_files_to_follow_up(followup)

        # emit signal when the PublicTicketForm.save is done
        new_ticket_done.send(sender="PublicTicketForm", ticket=ticket)

        self._send_messages(ticket=ticket, queue=queue, followup=followup, files=files)
        return ticket


class UserSettingsForm(forms.ModelForm):
    class Meta:
        model = UserSettings
        exclude = ["user", "settings_pickled"]


class EmailIgnoreForm(forms.ModelForm):
    class Meta:
        model = IgnoreEmail
        exclude = []


class TicketCCForm(forms.ModelForm):
    """Adds either an email address or helpdesk user as a CC on a Ticket. Used for processing POST requests."""

    class Meta:
        model = TicketCC
        exclude = ("ticket",)

    def __init__(self, *args, **kwargs):
        super(TicketCCForm, self).__init__(*args, **kwargs)
        self.fields["user"].queryset = get_assignable_users(
            helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC
        )


class TicketCCUserForm(forms.ModelForm):
    """Adds a helpdesk user as a CC on a Ticket"""

    def __init__(self, *args, **kwargs):
        super(TicketCCUserForm, self).__init__(*args, **kwargs)
        self.fields["user"].queryset = get_assignable_users(
            helpdesk_settings.HELPDESK_STAFF_ONLY_TICKET_CC
        )

    class Meta:
        model = TicketCC
        exclude = (
            "ticket",
            "email",
        )


class TicketCCEmailForm(forms.ModelForm):
    """Adds an email address as a CC on a Ticket"""

    def __init__(self, *args, **kwargs):
        super(TicketCCEmailForm, self).__init__(*args, **kwargs)

    class Meta:
        model = TicketCC
        exclude = (
            "ticket",
            "user",
        )


class TicketDependencyForm(forms.ModelForm):
    """Adds a different ticket as a dependency for this Ticket"""

    class Meta:
        model = TicketDependency
        fields = ("depends_on",)

    def __init__(self, ticket, *args, **kwargs):
        super(TicketDependencyForm, self).__init__(*args, **kwargs)

        # Only open tickets except myself, existing dependencies and parents
        self.fields["depends_on"].queryset = (
            Ticket.objects.filter(status__in=Ticket.OPEN_STATUSES)
            .exclude(id=ticket.id)
            .exclude(depends_on__ticket=ticket)
            .exclude(ticketdependency__depends_on=ticket)
        )


class TicketResolvesForm(forms.ModelForm):
    """Adds this ticket as a dependency for a different ticket"""

    class Meta:
        model = TicketDependency
        fields = ("ticket",)

    def __init__(self, ticket, *args, **kwargs):
        super(TicketResolvesForm, self).__init__(*args, **kwargs)

        # Only open tickets except myself, existing dependencies and parents
        self.fields["ticket"].queryset = (
            Ticket.objects.exclude(status__in=Ticket.OPEN_STATUSES)
            .exclude(id=ticket.id)
            .exclude(depends_on__ticket=ticket)
            .exclude(ticketdependency__depends_on=ticket)
        )


class MultipleTicketSelectForm(forms.Form):
    tickets = forms.ModelMultipleChoiceField(
        label=_("Tickets to merge"),
        queryset=Ticket.objects.filter(merged_to=None),
        widget=forms.SelectMultiple(attrs={"class": "form-control"}),
    )

    def clean_tickets(self):
        tickets = self.cleaned_data.get("tickets")
        if len(tickets) < 2:
            raise ValidationError(_("Please choose at least 2 tickets."))
        if len(tickets) > 4:
            raise ValidationError(_("Impossible to merge more than 4 tickets..."))
        queues = tickets.order_by("queue").distinct().values_list("queue", flat=True)
        if len(queues) != 1:
            raise ValidationError(
                _(
                    "All selected tickets must share the same queue in order to be merged."
                )
            )
        return tickets


class ChecklistTemplateForm(forms.ModelForm):
    name = forms.CharField(
        widget=forms.TextInput(attrs={"class": "form-control"}),
        required=True,
    )
    task_list = forms.JSONField(widget=forms.HiddenInput())

    class Meta:
        model = ChecklistTemplate
        fields = ("name", "task_list")

    def clean_task_list(self):
        task_list = self.cleaned_data["task_list"]
        return list(map(lambda task: task.strip(), task_list))


class ChecklistForm(forms.ModelForm):
    name = forms.CharField(
        widget=forms.TextInput(attrs={"class": "form-control"}),
        required=True,
    )

    class Meta:
        model = Checklist
        fields = ("name",)


class CreateChecklistForm(ChecklistForm):
    checklist_template = forms.ModelChoiceField(
        label=_("Template"),
        queryset=ChecklistTemplate.objects.all(),
        widget=forms.Select(attrs={"class": "form-control"}),
        required=False,
    )

    class Meta(ChecklistForm.Meta):
        fields = ("checklist_template", "name")


class FormControlDeleteFormSet(forms.BaseInlineFormSet):
    deletion_widget = forms.CheckboxInput(attrs={"class": "form-control"})
