import typing

from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.utils.translation import gettext as _

from helpdesk.lib import safe_template_context
from helpdesk import settings as helpdesk_settings
from helpdesk.lib import process_attachments
from helpdesk.decorators import (
    is_helpdesk_staff,
)
from helpdesk.models import (
    FollowUp,
    Ticket,
    TicketCC,
)
from helpdesk.signals import update_ticket_done

User = get_user_model()


def add_staff_subscription(user: User, ticket: Ticket) -> None:
    """Auto subscribe the staff member if that's what the settigs say and the
    user is authenticated and a staff member"""
    if (
        helpdesk_settings.HELPDESK_AUTO_SUBSCRIBE_ON_TICKET_RESPONSE
        and user.is_authenticated
        and return_ticketccstring_and_show_subscribe(user, ticket)[1]
    ):
        subscribe_to_ticket_updates(ticket, user.id)


def return_ticketccstring_and_show_subscribe(user, ticket):
    """used in view_ticket() and followup_edit()"""
    # create the ticketcc_string and check whether current user is already
    # subscribed
    username = user.get_username().upper()
    try:
        useremail = user.email.upper()
    except AttributeError:
        useremail = ""
    strings_to_check = list()
    strings_to_check.append(username)
    strings_to_check.append(useremail)

    ticketcc_string = ""
    all_ticketcc = ticket.ticketcc_set.all()
    counter_all_ticketcc = len(all_ticketcc) - 1
    show_subscribe = True
    for i, ticketcc in enumerate(all_ticketcc):
        ticketcc_this_entry = str(ticketcc.display)
        ticketcc_string += ticketcc_this_entry
        if i < counter_all_ticketcc:
            ticketcc_string += ", "
        if strings_to_check.__contains__(ticketcc_this_entry.upper()):
            show_subscribe = False

    # check whether current user is a submitter or assigned to ticket
    assignedto_username = str(ticket.assigned_to).upper()
    strings_to_check = list()
    if ticket.submitter_email is not None:
        submitter_email = ticket.submitter_email.upper()
        strings_to_check.append(submitter_email)
    strings_to_check.append(assignedto_username)
    if strings_to_check.__contains__(username) or strings_to_check.__contains__(
        useremail
    ):
        show_subscribe = False

    return ticketcc_string, show_subscribe


def subscribe_to_ticket_updates(
    ticket, user_id=None, email=None, can_view=True, can_update=False
):
    if ticket is not None:
        queryset = TicketCC.objects.filter(ticket=ticket, user_id=user_id, email=email)

        # Don't create duplicate entries for subscribers
        if queryset.count() > 0:
            return queryset.first()

        if user_id is None and len(email) < 5:
            raise ValidationError(
                _(
                    "When you add somebody on Cc, you must provide either a User or a valid email. Email: %s"
                    % email
                )
            )

        return ticket.ticketcc_set.create(
            user_id=user_id, email=email, can_view=can_view, can_update=can_update
        )


def get_and_set_ticket_status(
    new_status: int, ticket: Ticket, follow_up: FollowUp
) -> typing.Tuple[str, int]:
    """Performs comparision on previous status to new status,
    updating the title as required.

    Returns:
        The old status as a display string, old status code string
    """
    old_status_str = ticket.get_status_display()
    old_status = ticket.status
    if new_status != ticket.status:
        ticket.status = new_status
        ticket.save()
        follow_up.new_status = new_status
        if follow_up.title:
            follow_up.title += " and %s" % ticket.get_status_display()
        else:
            follow_up.title = "%s" % ticket.get_status_display()

    if not follow_up.title:
        if follow_up.comment:
            follow_up.title = _("Comment")
        else:
            follow_up.title = _("Updated")

    follow_up.save()
    return old_status_str, old_status


def process_email_notifications_for_ticket_update(
    public: bool,
    ticket: Ticket,
    follow_up: FollowUp,
    context: dict,
    messages_sent_to: typing.Set[str],
    files: typing.List[typing.Tuple[str, str]],
    reassigned: bool = False,
):
    """
    Sends email notifications when the ticket is updated in any way.
    """
    # If the setting is enabled and this is a private follow-up, skip all email notifications
    if helpdesk_settings.HELPDESK_PRIVATE_FOLLOWUP_MEANS_NO_EMAILS and not public:
        return

    template_prefix = get_email_template_prefix(reassigned, follow_up)
    if helpdesk_settings.HELPDESK_NOTIFY_SUBMITTER_FOR_ALL_TICKET_CHANGES or (
        public
        and (
            follow_up.comment
            or (follow_up.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS))
        )
    ):
        # Use hard coded prefix for submitter updates on  tickets for backwards compatibility
        # TODO: possibly make the template prefix modification configurable
        messages_sent_to.update(
            ticket.send(
                {
                    "submitter": ("updated_submitter", context),
                },
                dont_send_to=messages_sent_to,
                fail_silently=True,
                files=files,
            )
        )
    if ticket.assigned_to and (
        ticket.assigned_to.usersettings_helpdesk.email_on_ticket_change
        or (
            reassigned
            and ticket.assigned_to.usersettings_helpdesk.email_on_ticket_assign
        )
    ):
        messages_sent_to.update(
            ticket.send(
                {"assigned_to": (template_prefix + "owner", context)},
                dont_send_to=messages_sent_to,
                fail_silently=True,
                files=files,
            )
        )

    messages_sent_to.update(
        ticket.send(
            {"ticket_cc": (template_prefix + "cc", context)},
            dont_send_to=messages_sent_to,
            fail_silently=True,
            files=files,
        )
    )


def get_email_template_prefix(reassigned, follow_up: FollowUp) -> str:
    if reassigned:
        return "assigned_"
    elif follow_up.new_status == Ticket.RESOLVED_STATUS:
        return "resolved_"
    elif follow_up.new_status == Ticket.CLOSED_STATUS:
        return "closed_"
    else:
        return "updated_"


def update_ticket(
    user,
    ticket,
    title=None,
    comment="",
    files=None,
    public=False,
    owner=-1,
    priority=-1,
    queue=-1,
    new_status=None,
    time_spent=None,
    due_date=None,
    new_checklists=None,
    message_id=None,
    customfields_form=None,
):
    # We need to allow the 'ticket' and 'queue' contexts to be applied to the
    # comment.
    context = safe_template_context(ticket)
    if title is None:
        title = ticket.title
    if priority == -1:
        priority = ticket.priority
    if queue == -1:
        queue = ticket.queue.id
    if new_status is None:
        new_status = ticket.status
    if new_checklists is None:
        new_checklists = {}

    from django.template import engines

    template_func = engines["django"].from_string
    # this prevents system from trying to render any template tags
    # broken into two stages to prevent changes from first replace being themselves
    # changed by the second replace due to conflicting syntax
    comment = comment.replace("{%", "X-HELPDESK-COMMENT-VERBATIM").replace(
        "%}", "X-HELPDESK-COMMENT-ENDVERBATIM"
    )
    comment = comment.replace(
        "X-HELPDESK-COMMENT-VERBATIM", "{% verbatim %}{%"
    ).replace("X-HELPDESK-COMMENT-ENDVERBATIM", "%}{% endverbatim %}")
    # render the neutralized template
    comment = template_func(comment).render(context)

    if owner == -1 and ticket.assigned_to:
        owner = ticket.assigned_to.id

    f = FollowUp(
        ticket=ticket,
        date=timezone.now(),
        comment=comment,
        time_spent=time_spent,
        message_id=message_id,
        title=title,
    )

    if is_helpdesk_staff(user):
        f.user = user

    f.public = public

    reassigned = False

    old_owner = ticket.assigned_to
    if owner != -1:
        if owner != 0 and (
            (ticket.assigned_to and owner != ticket.assigned_to.id)
            or not ticket.assigned_to
        ):
            new_user = User.objects.get(id=owner)
            f.title = _("Assigned to %(username)s") % {
                "username": new_user.get_username(),
            }
            ticket.assigned_to = new_user
            reassigned = True
        # user changed owner to 'unassign'
        elif owner == 0 and ticket.assigned_to is not None:
            f.title = _("Unassigned")
            ticket.assigned_to = None

    old_status_str, old_status = get_and_set_ticket_status(new_status, ticket, f)

    files = process_attachments(f, files) if files else []

    if title and title != ticket.title:
        f.ticketchange_set.create(
            field=_("Title"),
            old_value=ticket.title,
            new_value=title,
        )
        ticket.title = title

    if new_status != old_status:
        f.ticketchange_set.create(
            field=_("Status"),
            old_value=old_status_str,
            new_value=ticket.get_status_display(),
        )

    if ticket.assigned_to != old_owner:
        f.ticketchange_set.create(
            field=_("Owner"),
            old_value=old_owner,
            new_value=ticket.assigned_to if ticket.assigned_to else _("Unassigned"),
        )

    if priority != ticket.priority:
        f.ticketchange_set.create(
            field=_("Priority"),
            old_value=ticket.priority,
            new_value=priority,
        )
        ticket.priority = priority

    if queue != ticket.queue.id:
        f.ticketchange_set.create(
            field=_("Queue"),
            old_value=ticket.queue.id,
            new_value=queue,
        )
        ticket.queue_id = queue

    if due_date and due_date != ticket.due_date:
        f.ticketchange_set.create(
            field=_("Due on"),
            old_value=ticket.due_date,
            new_value=due_date,
        )
        ticket.due_date = due_date

    # save custom fields and ticket changes
    if customfields_form and customfields_form.is_valid():
        customfields_form.save(followup=f)

    for checklist in ticket.checklists.all():
        if checklist.id not in new_checklists:
            continue
        new_completed_tasks = new_checklists[checklist.id]
        for task in checklist.tasks.all():
            changed = None

            # Add completion if it was not done yet
            if not task.completion_date and task.id in new_completed_tasks:
                task.completion_date = timezone.now()
                changed = "completed"
            # Remove it if it was done before
            elif task.completion_date and task.id not in new_completed_tasks:
                task.completion_date = None
                changed = "uncompleted"

            # Save and add ticket change if task state has changed
            if changed:
                task.save(update_fields=["completion_date"])
                f.ticketchange_set.create(
                    field=f"[{checklist.name}] {task.description}",
                    old_value=_("To do") if changed == "completed" else _("Completed"),
                    new_value=_("Completed") if changed == "completed" else _("To do"),
                )

    if new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS) and (
        new_status == Ticket.RESOLVED_STATUS or ticket.resolution is None
    ):
        ticket.resolution = comment

    # ticket might have changed above, so we re-instantiate context with the
    # (possibly) updated ticket.
    context = safe_template_context(ticket)
    context.update(
        resolution=ticket.resolution,
        comment=f.comment,
    )

    messages_sent_to = set()
    try:
        messages_sent_to.add(user.email)
    except AttributeError:
        pass
    process_email_notifications_for_ticket_update(
        public, ticket, f, context, messages_sent_to, files, reassigned=reassigned
    )
    ticket.save()

    # emit signal with followup when the ticket update is done
    # internally used for webhooks
    update_ticket_done.send(sender="update_ticket", followup=f)

    # auto subscribe user if enabled
    add_staff_subscription(user, ticket)
    return f
