import logging
from collections import defaultdict

import iso639
from dash.orgs.views import OrgObjPermsMixin, OrgPermsMixin
from el_pagination.paginators import LazyPaginator
from smartmin.csv_imports.models import ImportTask
from smartmin.mixins import NonAtomicMixin
from smartmin.views import (
    SmartCreateView,
    SmartCRUDL,
    SmartCSVImportView,
    SmartDeleteView,
    SmartListView,
    SmartReadView,
    SmartTemplateView,
    SmartUpdateView,
)
from temba_client.utils import parse_iso8601

from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.urls import reverse
from django.utils.timesince import timesince
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

from casepro.rules.mixins import RuleFormMixin
from casepro.statistics.models import DailyCount
from casepro.utils import JSONEncoder, json_encode, month_range, str_to_bool
from casepro.utils.export import BaseDownloadView

from .forms import FaqForm, LabelForm
from .models import FAQ, Label, Message, MessageExport, MessageFolder, Outgoing, OutgoingFolder, ReplyExport
from .tasks import message_export, reply_export

RESPONSE_DELAY_WARN_SECONDS = 24 * 60 * 60  # show response delays > 1 day as warning

logger = logging.getLogger(__name__)


# Override the ImportTask start method so we can use our self-defined task
def override_start(self, org):  # pragma: no cover
    from .tasks import faq_csv_import

    self.log("Queued import at %s" % now())
    self.save(update_fields=("import_log",))

    # trigger task
    result = faq_csv_import.delay(org.id, self.id)

    self.task_id = result.task_id
    self.save(update_fields=("task_id",))


ImportTask.start = override_start


class LabelCRUDL(SmartCRUDL):
    actions = ("create", "update", "read", "delete", "list", "watch", "unwatch")
    model = Label

    class Create(RuleFormMixin, OrgPermsMixin, SmartCreateView):
        form_class = LabelForm

        def get_form_kwargs(self):
            kwargs = super(LabelCRUDL.Create, self).get_form_kwargs()
            kwargs["org"] = self.request.org
            kwargs["is_create"] = True
            return kwargs

        def derive_initial(self):
            # label created manually in casepro aren't synced by default
            initial = super(LabelCRUDL.Create, self).derive_initial()
            initial["is_synced"] = False
            return initial

        def save(self, obj):
            data = self.form.cleaned_data
            org = self.request.org
            name = data["name"]
            description = data["description"]
            tests = self.construct_tests()
            is_synced = data["is_synced"]

            self.object = Label.create(org, name, description, tests, is_synced)

        def get_success_url(self):
            return reverse("msgs.label_read", args=[self.object.pk])

    class Update(RuleFormMixin, OrgObjPermsMixin, SmartUpdateView):
        form_class = LabelForm
        success_url = "id@msgs.label_read"

        def get_form_kwargs(self):
            kwargs = super(LabelCRUDL.Update, self).get_form_kwargs()
            kwargs["org"] = self.request.org
            kwargs["is_create"] = False
            return kwargs

        def post_save(self, obj):
            obj = super(LabelCRUDL.Update, self).post_save(obj)

            tests = self.construct_tests()
            obj.update_tests(tests)

            return obj

    class Read(OrgObjPermsMixin, SmartReadView):
        def get_queryset(self):
            return Label.get_all(self.request.org, self.request.user)

        def get_context_data(self, **kwargs):
            context = super(LabelCRUDL.Read, self).get_context_data(**kwargs)

            # augment usual label JSON
            label_json = self.object.as_json()
            label_json["watching"] = self.object.is_watched_by(self.request.user)

            # angular app requires context data in JSON format
            context["context_data_json"] = json_encode({"label": label_json})

            context["rule_tests"] = self.object.rule.get_tests_description() if self.object.rule else ""

            return context

    class Delete(OrgObjPermsMixin, SmartDeleteView):
        cancel_url = "@msgs.label_list"

        def post(self, request, *args, **kwargs):
            label = self.get_object()
            label.release()
            return HttpResponse(status=204)

    class List(OrgPermsMixin, SmartListView):
        def get(self, request, *args, **kwargs):
            with_activity = str_to_bool(self.request.GET.get("with_activity", ""))
            labels = list(Label.get_all(self.request.org, self.request.user).order_by("name"))
            Label.bulk_cache_initialize(labels)

            if with_activity:
                # get message statistics
                this_month = DailyCount.get_by_label(labels, DailyCount.TYPE_INCOMING, *month_range(0)).scope_totals()
                last_month = DailyCount.get_by_label(labels, DailyCount.TYPE_INCOMING, *month_range(-1)).scope_totals()

            def as_json(label):
                obj = label.as_json()
                if with_activity:
                    obj["activity"] = {"this_month": this_month.get(label, 0), "last_month": last_month.get(label, 0)}
                return obj

            return JsonResponse({"results": [as_json(l) for l in labels]})

    class Watch(OrgObjPermsMixin, SmartReadView):
        """
        Endpoint for watching a label
        """

        permission = "msgs.label_read"

        def post(self, request, *args, **kwargs):
            self.get_object().watch(request.user)
            return HttpResponse(status=204)

    class Unwatch(OrgObjPermsMixin, SmartReadView):
        """
        Endpoint for unwatching a label
        """

        permission = "msgs.label_read"

        def post(self, request, *args, **kwargs):
            self.get_object().unwatch(request.user)
            return HttpResponse(status=204)


class MessageSearchMixin(object):
    def derive_search(self):
        """
        Collects and prepares message search parameters into JSON serializable dict
        """
        folder = MessageFolder[self.request.GET["folder"]]
        if folder == MessageFolder.flagged and str_to_bool(self.request.GET.get("archived", "")):
            folder = MessageFolder.flagged_with_archived

        label_id = self.request.GET.get("label", None)
        text = self.request.GET.get("text", None)
        contact_id = self.request.GET.get("contact", None)
        after = parse_iso8601(self.request.GET.get("after", None))
        before = parse_iso8601(self.request.GET.get("before", None))

        return {
            "folder": folder,
            "label": label_id,
            "text": text,
            "contact": contact_id,
            "after": after,
            "before": before,
        }


class MessageCRUDL(SmartCRUDL):
    actions = ("search", "lock", "action", "label", "bulk_reply", "forward", "history")
    model = Message

    class Search(OrgPermsMixin, MessageSearchMixin, SmartTemplateView):
        """
        JSON endpoint for fetching incoming messages
        """

        page_size = 50

        def get_messages(self, search, last_refresh=None):
            org = self.request.org
            user = self.request.user
            queryset = Message.search(org, user, search, modified_after=last_refresh, all=False)
            return queryset.prefetch_related("contact", "labels", "case__assignee", "case__user_assignee")

        def get_context_data(self, **kwargs):
            context = super(MessageCRUDL.Search, self).get_context_data(**kwargs)

            page = int(self.request.GET.get("page", 1))
            last_refresh = self.request.GET.get("last_refresh")

            search = self.derive_search()

            # this is a refresh of new and modified messages
            if last_refresh:
                messages = self.get_messages(search, last_refresh)

                # don't use paging for these messages
                context["object_list"] = list(messages)
                context["has_more"] = False
            else:
                messages = self.get_messages(search)
                paginator = LazyPaginator(messages, per_page=self.page_size)

                context["object_list"] = paginator.page(page)
                context["has_more"] = paginator.num_pages > page

            return context

        def render_to_response(self, context, **response_kwargs):
            results = []
            for m in context["object_list"]:
                msg = m.as_json()

                msg["lock"] = m.get_lock(self.request.user)

                results.append(msg)

            return JsonResponse({"results": results, "has_more": context["has_more"]}, encoder=JSONEncoder)

    class Lock(OrgPermsMixin, SmartTemplateView):
        """
        AJAX endpoint for updating messages with a date and user id.
        Takes a list of message ids.
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/lock/(?P<action>\w+)/$"

        def post(self, request, *args, **kwargs):
            org = request.org
            user = request.user

            action = kwargs["action"]

            message_ids = request.json["messages"]
            messages = org.incoming_messages.filter(org=org, backend_id__in=message_ids)

            lock_messages = []

            if action == "lock":
                for message in messages:
                    if message.get_lock(request.user):
                        lock_messages.append(message.backend_id)

                if not lock_messages:
                    for message in messages:
                        message.user_lock(user)

            elif action == "unlock":
                for message in messages:
                    message.user_unlock()

            else:  # pragma: no cover
                return HttpResponseBadRequest("Invalid action: %s", action)

            return JsonResponse({"messages": lock_messages}, encoder=JSONEncoder)

    class Action(OrgPermsMixin, SmartTemplateView):
        """
        AJAX endpoint for bulk message actions. Takes a list of message ids.
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/action/(?P<action>\w+)/$"

        def post(self, request, *args, **kwargs):
            org = request.org
            user = request.user

            action = kwargs["action"]

            message_ids = request.json["messages"]
            messages = org.incoming_messages.filter(org=org, backend_id__in=message_ids)

            label_id = request.json.get("label")
            label = Label.get_all(org, user).get(pk=label_id) if label_id else None

            if action == "flag":
                Message.bulk_flag(org, user, messages)
            elif action == "unflag":
                Message.bulk_unflag(org, user, messages)
            elif action == "label":
                Message.bulk_label(org, user, messages, label)
            elif action == "unlabel":
                Message.bulk_unlabel(org, user, messages, label)
            elif action == "archive":
                Message.bulk_archive(org, user, messages)
            elif action == "restore":
                Message.bulk_restore(org, user, messages)
            else:  # pragma: no cover
                return HttpResponseBadRequest("Invalid action: %s", action)

            return HttpResponse(status=204)

    class Label(OrgPermsMixin, SmartTemplateView):
        """
        AJAX endpoint for labelling a message.
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/label/(?P<id>\d+)/$"

        def post(self, request, *args, **kwargs):
            org = request.org
            user = request.user
            user_labels = Label.get_all(self.org, user)

            message_id = int(kwargs["id"])
            message = org.incoming_messages.filter(org=org, backend_id=message_id).first()

            label_ids = request.json["labels"]
            specified_labels = list(user_labels.filter(pk__in=label_ids))

            # user can't remove labels that they can't see
            unseen_labels = [l for l in message.labels.all() if l not in user_labels]

            message.update_labels(user, specified_labels + unseen_labels)

            return HttpResponse(status=204)

    class BulkReply(OrgPermsMixin, SmartTemplateView):
        """
        JSON endpoint for bulk messages replies
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/bulk_reply/$"

        def post(self, request, *args, **kwargs):
            text = request.json["text"]
            message_ids = request.json["messages"]
            messages = Message.objects.filter(org=request.org, backend_id__in=message_ids).select_related("contact")

            # organize messages by contact
            messages_by_contact = defaultdict(list)
            for msg in messages:
                messages_by_contact[msg.contact].append(msg)

            # the actual message that will be replied to is the oldest selected message for each contact
            reply_tos = []
            for contact, contact_messages in messages_by_contact.items():
                contact_messages = sorted(contact_messages, key=lambda m: m.created_on, reverse=True)
                reply_tos.append(contact_messages[0])

            outgoing = Outgoing.create_bulk_replies(request.org, request.user, text, reply_tos)
            return JsonResponse({"messages": len(outgoing)})

    class Forward(OrgPermsMixin, SmartTemplateView):
        """
        JSON endpoint for forwarding a message to a URN
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/forward/(?P<id>\d+)/$"

        def post(self, request, *args, **kwargs):
            text = request.json["text"]
            message = Message.objects.get(org=request.org, backend_id=int(kwargs["id"]))
            urns = request.json["urns"]

            outgoing = Outgoing.create_forwards(request.org, request.user, text, urns, message)
            return JsonResponse({"messages": len(outgoing)})

    class History(OrgPermsMixin, SmartTemplateView):
        """
        JSON endpoint for fetching message history. Takes a message backend id
        """

        @classmethod
        def derive_url_pattern(cls, path, action):
            return r"^message/history/(?P<id>\d+)/$"

        def get(self, request, *args, **kwargs):
            message = Message.objects.get(org=request.org, backend_id=int(kwargs["id"]))
            actions = [a.as_json() for a in message.get_history()]
            return JsonResponse({"actions": actions}, encoder=JSONEncoder)


class MessageExportCRUDL(SmartCRUDL):
    model = MessageExport
    actions = ("create", "read")

    class Create(NonAtomicMixin, OrgPermsMixin, MessageSearchMixin, SmartCreateView):
        def post(self, request, *args, **kwargs):
            search = self.derive_search()
            export = MessageExport.create(self.request.org, self.request.user, search)

            message_export.delay(export.pk)

            return JsonResponse({"export_id": export.pk})

    class Read(BaseDownloadView):
        title = _("Download Messages")
        filename = "message_export.xls"


class ReplySearchMixin(object):
    def derive_search(self):
        """
        Collects and prepares reply search parameters into JSON serializable dict
        """
        params = self.request.GET
        partner = params.get("partner")
        after = parse_iso8601(params.get("after"))
        before = parse_iso8601(params.get("before"))

        return {"partner": partner, "after": after, "before": before}


class OutgoingCRUDL(SmartCRUDL):
    actions = ("search", "search_replies")
    model = Outgoing

    class Search(OrgPermsMixin, SmartTemplateView):
        """
        JSON endpoint for fetching outgoing messages
        """

        def derive_search(self):
            folder = OutgoingFolder[self.request.GET["folder"]]
            text = self.request.GET.get("text", None)
            contact = self.request.GET.get("contact", None)

            return {"folder": folder, "text": text, "contact": contact}

        def get_context_data(self, **kwargs):
            context = super(OutgoingCRUDL.Search, self).get_context_data(**kwargs)

            org = self.request.org
            user = self.request.user
            page = int(self.request.GET.get("page", 1))

            search = self.derive_search()
            messages = Outgoing.search(org, user, search)
            paginator = LazyPaginator(messages, per_page=50)

            context["object_list"] = paginator.page(page)
            context["has_more"] = paginator.num_pages > page
            return context

        def render_to_response(self, context, **response_kwargs):
            return JsonResponse(
                {"results": [m.as_json() for m in context["object_list"]], "has_more": context["has_more"]},
                encoder=JSONEncoder,
            )

    class SearchReplies(OrgPermsMixin, ReplySearchMixin, SmartTemplateView):
        """
        JSON endpoint to fetch replies made by users
        """

        def get(self, request, *args, **kwargs):
            org = self.request.org
            user = self.request.user
            page = int(self.request.GET.get("page", 1))

            search = self.derive_search()
            items = Outgoing.search_replies(org, user, search)

            paginator = LazyPaginator(items, 50)
            outgoing = paginator.page(page)
            has_more = paginator.num_pages > page

            def as_json(msg):
                delay = (msg.created_on - msg.reply_to.created_on).total_seconds()
                obj = msg.as_json()
                obj.update(
                    {
                        "reply_to": {
                            "text": msg.reply_to.text,
                            "flagged": msg.reply_to.is_flagged,
                            "labels": [l.as_json(full=False) for l in msg.reply_to.labels.all()],
                        },
                        "response": {
                            "delay": timesince(msg.reply_to.created_on, now=msg.created_on),
                            "warning": delay > RESPONSE_DELAY_WARN_SECONDS,
                        },
                    }
                )
                return obj

            return JsonResponse({"results": [as_json(o) for o in outgoing], "has_more": has_more}, encoder=JSONEncoder)


class ReplyExportCRUDL(SmartCRUDL):
    model = ReplyExport
    actions = ("create", "read")

    class Create(NonAtomicMixin, OrgPermsMixin, ReplySearchMixin, SmartCreateView):
        def post(self, request, *args, **kwargs):
            search = self.derive_search()
            export = self.model.create(self.request.org, self.request.user, search)

            reply_export.delay(export.pk)

            return JsonResponse({"export_id": export.pk})

    class Read(BaseDownloadView):
        title = _("Download Replies")
        filename = "reply_export.xls"


class FaqSearchMixin(object):
    def derive_search(self):
        """
        Collects and prepares FAQ search parameters into JSON serializable dict
        """
        label = self.request.GET.get("label", None)
        text = self.request.GET.get("text", None)
        language = self.request.GET.get("language", None)

        return {"label": label, "text": text, "language": language}


class FaqCRUDL(SmartCRUDL):
    model = FAQ
    actions = ("list", "create", "read", "update", "delete", "search", "import", "languages")

    class List(OrgPermsMixin, SmartListView):
        fields = ("question", "answer", "language", "parent")
        default_order = ("-parent", "question")

        def derive_queryset(self, **kwargs):
            return FAQ.get_all(self.request.org)

    class Create(OrgPermsMixin, SmartCreateView):
        form_class = FaqForm

        def get_form_kwargs(self):
            kwargs = super(FaqCRUDL.Create, self).get_form_kwargs()
            # Get the data for post requests that didn't come through a form
            if self.request.method == "POST" and not self.request.POST and hasattr(self.request, "json"):
                kwargs["data"] = self.request.json
            kwargs["org"] = self.request.org
            return kwargs

        def save(self, obj):
            data = self.form.cleaned_data
            org = self.request.org
            question = data["question"]
            answer = data["answer"]
            language = data["language"]
            parent = data["parent"]
            labels = data["labels"]

            faq = FAQ.create(org, question, answer, language, parent, labels)
            self.object = faq

    class Read(OrgPermsMixin, SmartReadView):
        fields = ["question", "answer", "language", "parent"]

        def derive_queryset(self, **kwargs):
            return FAQ.get_all(self.request.org)

        def get_context_data(self, **kwargs):
            context = super(FaqCRUDL.Read, self).get_context_data(**kwargs)
            edit_button_url = reverse("msgs.faq_update", args=[self.object.pk])
            context["context_data_json"] = json_encode({"faq": self.object.as_json()})
            context["edit_button_url"] = edit_button_url
            context["can_delete"] = True

            labels = []
            for label in self.object.labels.all():
                labels.append(label.name)
            context["labels"] = ", ".join(labels)
            return context

    class Update(OrgPermsMixin, SmartUpdateView):
        form_class = FaqForm

        def get_form_kwargs(self):
            kwargs = super(FaqCRUDL.Update, self).get_form_kwargs()
            # Get the data for post requests that didn't come through a form
            if self.request.method == "POST" and not self.request.POST and hasattr(self.request, "json"):
                kwargs["data"] = self.request.json
            kwargs["org"] = self.request.org
            return kwargs

        def derive_initial(self):
            initial = super(FaqCRUDL.Update, self).derive_initial()
            initial["labels"] = self.object.labels.all()
            return initial

        def derive_fields(self):
            fields = ["question", "answer", "language", "parent"]
            if not self.object.parent:
                fields.append("labels")

            return tuple(fields)

    class Delete(OrgPermsMixin, SmartDeleteView):
        cancel_url = "@msgs.faq_list"

        def post(self, request, *args, **kwargs):
            faq = self.get_object()
            faq.release()

            return HttpResponse(status=204)

    class Search(OrgPermsMixin, FaqSearchMixin, SmartTemplateView):
        """
        JSON endpoint for searching FAQs
        """

        def get_context_data(self, **kwargs):
            context = super(FaqCRUDL.Search, self).get_context_data(**kwargs)

            org = self.request.org
            user = self.request.user

            search = self.derive_search()
            faqs = FAQ.search(org, user, search)
            context["object_list"] = faqs
            return context

        def render_to_response(self, context, **response_kwargs):
            return JsonResponse({"results": [m.as_json() for m in context["object_list"]]}, encoder=JSONEncoder)

    class Import(OrgPermsMixin, SmartCSVImportView):
        model = ImportTask
        fields = ("csv_file",)
        success_message = "File uploaded successfully. If your FAQs don't appear here soon, something went wrong."
        success_url = "@msgs.faq_list"

        def post_save(self, task):
            task.start(self.org)
            return task

    class Languages(OrgPermsMixin, SmartTemplateView):
        """
        JSON endpoint for getting a list of currently all available languages
        """

        def get_context_data(self, **kwargs):
            context = super(FaqCRUDL.Languages, self).get_context_data(**kwargs)

            org = self.request.org
            langs = FAQ.get_all_languages(org)
            lang_list = []
            for lang in langs:
                lang_list.append(FAQ.get_language_from_code(lang["language"]))
            context["language_list"] = lang_list

            iso_list = [{"name": l.name, "code": l.part3} for l in iso639.languages]

            context["iso_list"] = iso_list
            return context

        def render_to_response(self, context, **response_kwargs):
            return JsonResponse(
                {"results": context["language_list"], "iso_list": context["iso_list"]}, encoder=JSONEncoder
            )