"""
ARCHES - a program developed to inventory and manage immovable cultural heritage.
Copyright (C) 2013 J. Paul Getty Trust and World Monuments Fund

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

import importlib
import json as jsonparser
import logging
import traceback
import uuid
import arches.app.utils.zip as arches_zip
from arches.app.datatypes.datatypes import DataTypeFactory
from arches.app.models import models
from arches.app.models.resource import Resource, PublishedModelError
from arches.app.models.tile import Tile, TileValidationError
from arches.app.models.system_settings import settings
from arches.app.utils.response import JSONResponse, JSONErrorResponse
from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer
from arches.app.utils.decorators import can_edit_resource_instance
from arches.app.utils.permission_backend import user_is_resource_reviewer
from django.contrib.auth.models import User
from django.http import HttpResponseNotFound, HttpResponseBadRequest
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.views.generic import View
from django.db import transaction
from django.shortcuts import redirect
from arches.app.models.resource import EditLog

logger = logging.getLogger(__name__)


@method_decorator(can_edit_resource_instance, name="dispatch")
class TileData(View):
    action = "update_tile"

    def delete_provisional_edit(self, tile, user, request):
        provisionaledits = None
        if tile.provisionaledits is not None:
            provisionaledits = tile.provisionaledits
            if user in provisionaledits:
                provisional_editor = User.objects.get(pk=user)
                edit = provisionaledits[user]
                provisionaledits.pop(user)
                if len(provisionaledits) == 0:
                    tile.provisionaledits = None
                else:
                    tile.provisionaledits = provisionaledits
                tile.save(
                    request=request,
                    provisional_edit_log_details={
                        "user": request.user,
                        "action": "delete edit",
                        "edit": edit,
                        "provisional_editor": provisional_editor,
                    },
                )

    def handle_save_error(self, e, tile_id="", title=_("Saving tile failed"), message=None):
        if message is None:
            message = type(e).__name__
            if hasattr(e, "message") and e.message:
                message += ": {0}".format(e.message)
        else:
            message = str(e)
        logger.error(
            title
            + """ [Tile id: {tile_id}] \
                     [Exception message: {message}] \
                     [Exception trace: {trace}]""".format(
                tile_id=tile_id, message=message, trace=traceback.format_exc()
            )
        )

        return JSONErrorResponse(_(title), _(str(message)), {"message": message, "title": title})

    def post(self, request):
        original_transaction_id = request.POST.get("transaction_id", None)
        transaction_id = request.POST.get("transaction_id", uuid.uuid1())

        if self.action == "update_tile":
            json = request.POST.get("data", None)
            accepted_provisional = request.POST.get("accepted_provisional", None)
            if accepted_provisional is not None:
                accepted_provisional_edit = JSONDeserializer().deserialize(accepted_provisional)
            if json is not None:
                data = JSONDeserializer().deserialize(json)
                data["resourceinstance_id"] = "" if "resourceinstance_id" not in data else data["resourceinstance_id"]

                if original_transaction_id is None:
                    data["transaction_id"] = None if "transaction_id" not in data else data["transaction_id"]
                    if data["transaction_id"] is not None:
                        transaction_id = data["transaction_id"]

                if data["resourceinstance_id"] == "":
                    data["resourceinstance_id"] = uuid.uuid4()
                try:
                    models.ResourceInstance.objects.get(pk=data["resourceinstance_id"])
                except ObjectDoesNotExist:
                    try:
                        resource = Resource(uuid.UUID(str(data["resourceinstance_id"])))
                    except ValueError:
                        resource = Resource()
                    graphid = models.Node.objects.filter(nodegroup=data["nodegroup_id"])[0].graph_id
                    resource.graph_id = graphid
                    try:
                        resource.save(user=request.user, transaction_id=transaction_id)
                        data["resourceinstance_id"] = resource.pk
                        resource.index()
                    except PublishedModelError as e:
                        message = _("Unable to save. Please verify the model is currently unpublished.")
                        return JSONResponse({"status": "false", "message": [_(e.title), _(str(message))]}, status=500)
                tile_id = data["tileid"]
                resource_instance = models.ResourceInstance.objects.get(pk=data["resourceinstance_id"])
                is_active = resource_instance.graph.publication is not None
                if tile_id is not None and tile_id != "":
                    try:
                        old_tile = Tile.objects.get(pk=tile_id)
                    except ObjectDoesNotExist as e:
                        return self.handle_save_error(e, _("This tile is no longer available"), _("It was likely deleted by another user"))

                tile = Tile(data)

                if tile.filter_by_perm(request.user, "write_nodegroup") and is_active is True:
                    try:
                        with transaction.atomic():
                            try:
                                if accepted_provisional is None:
                                    try:
                                        tile.save(request=request, transaction_id=transaction_id)
                                    except TileValidationError as e:
                                        resource_tiles_exist = models.TileModel.objects.filter(resourceinstance=tile.resourceinstance).exists()
                                        if not resource_tiles_exist:
                                            Resource.objects.get(pk=tile.resourceinstance_id).delete(request.user)
                                        title = _("Unable to save. Please verify your input is valid")
                                        return self.handle_save_error(e, tile_id, title=title)
                                    except PublishedModelError as e:
                                        message = _("Unable to save. Please verify the model is not currently published.")
                                        return JSONResponse({"status": "false", "message": [_(e.title), _(str(message))]}, status=500)
                                    except Exception as e:
                                        title = _("Unable to save.")
                                        return self.handle_save_error(e, tile_id, title=title, message=str(e))
                                else:
                                    if accepted_provisional is not None:
                                        provisional_editor = User.objects.get(pk=accepted_provisional_edit["user"])
                                        prov_edit_log_details = {
                                            "user": request.user,
                                            "action": "accept edit",
                                            "edit": accepted_provisional_edit,
                                            "provisional_editor": provisional_editor,
                                        }
                                    tile.save(request=request, provisional_edit_log_details=prov_edit_log_details)

                                if tile.provisionaledits is not None and str(request.user.id) in tile.provisionaledits:
                                    tile.data = tile.provisionaledits[str(request.user.id)]["value"]

                            except Exception as e:
                                return self.handle_save_error(e, tile_id)

                            tile.after_update_all()
                            update_system_settings_cache(tile)

                    except Exception as e:
                        return self.handle_save_error(e, tile_id)

                    return JSONResponse(tile)
                elif is_active is False:
                    response = {"status": "false", "message": [_("Request Failed"), _("Unable to Save. Verify model status is active")]}
                    return JSONResponse(response, status=500)
                else:
                    return JSONErrorResponse(_("Request Failed"), _("Permission Denied"))

        if self.action == "reorder_tiles":
            json = request.body
            if json is not None:
                data = JSONDeserializer().deserialize(json)

                if "tiles" in data and len(data["tiles"]) > 0:
                    sortorder = 0
                    with transaction.atomic():
                        for tile in data["tiles"]:
                            t = Tile(tile)
                            if t.filter_by_perm(request.user, "write_nodegroup"):
                                t.sortorder = sortorder
                                t.save(update_fields=["sortorder"], request=request)
                                sortorder = sortorder + 1
                    return JSONResponse(data)

        if self.action == "delete_provisional_tile":
            user = request.POST.get("user", None)
            tileid = request.POST.get("tileid", None)
            users = request.POST.get("users", None)
            tile = Tile.objects.get(tileid=tileid)
            is_provisional = tile.is_provisional()

            if tileid is not None and user is not None:
                provisionaledits = self.delete_provisional_edit(tile, user, request)

            elif tileid is not None and users is not None:
                users = jsonparser.loads(users)
                for user in users:
                    self.delete_provisional_edit(tile, user, request)

            if is_provisional == True:
                return JSONResponse({"result": "delete"})
            else:
                return JSONResponse({"result": "success"})

        return HttpResponseNotFound()

    def delete(self, request):
        json = request.body
        if json is not None:
            data = JSONDeserializer().deserialize(json)
            with transaction.atomic():
                try:
                    tile = Tile.objects.get(tileid=data["tileid"])
                    resource_instance = tile.resourceinstance
                    is_active = resource_instance.graph.publication_id is not None
                except ObjectDoesNotExist:
                    return JSONErrorResponse(_("This tile is no longer available"), _("It was likely already deleted by another user"))
                user_is_reviewer = user_is_resource_reviewer(request.user)
                if (user_is_reviewer or tile.is_provisional() is True) and is_active is True:
                    if tile.filter_by_perm(request.user, "delete_nodegroup"):
                        if tile.is_provisional() is True and len(list(tile.provisionaledits.keys())) == 1:
                            provisional_editor_id = list(tile.provisionaledits.keys())[0]
                            edit = tile.provisionaledits[provisional_editor_id]
                            provisional_editor = User.objects.get(pk=provisional_editor_id)
                            reviewer = request.user
                            tile.delete(
                                request=request,
                                provisional_edit_log_details={
                                    "user": reviewer,
                                    "action": "delete edit",
                                    "edit": edit,
                                    "provisional_editor": provisional_editor,
                                },
                            )
                        else:
                            tile.delete(request=request)
                        tile.after_update_all()
                        update_system_settings_cache(tile)
                        return JSONResponse(tile)
                    else:
                        return JSONErrorResponse(_("Request Failed"), _("Permission Denied"))
                elif is_active is False:
                    response = {"status": "false", "message": [_("Request Failed"), _("Unable to delete. Verify model status is active")]}
                    return JSONResponse(response, status=500)
                else:
                    return JSONErrorResponse(
                        _("Request Failed"), _("You do not have permissions to delete a tile with authoritative data.")
                    )

        return HttpResponseNotFound()

    def download_files(self, request):
        try:
            tileids = jsonparser.loads(request.GET.get("tiles", None))
            nodeid = request.GET.get("node", None)
            tiles = Tile.objects.filter(pk__in=tileids)
            files = sum(
                [
                    [{"name": file["name"], "outputfile": models.File.objects.get(pk=file["file_id"]).path} for file in tile.data[nodeid]]
                    for tile in tiles
                ],
                [],
            )

            file_objects = sum(
                [[models.File.objects.get(pk=file["file_id"]).path for file in tile.data[nodeid]] for tile in tiles],
                [],
            )

            file_size = 0
            num_files = 0
            last_file = None

            for file in file_objects:
                num_files += 1
                last_file = file
                file_size += file.size

            if num_files == 1:
                try:
                    last_file_url = last_file.url
                    return redirect(last_file_url)
                except:
                    pass

            if file_size < settings.FILE_VIEWER_DOWNLOAD_LIMIT:
                response = arches_zip.zip_response(files, "file-viewer-download.zip")
                return response
            else:
                return HttpResponseBadRequest(_("Too large to be zipped.  Try downloading a single file."))

        except TypeError as e:
            logger.error("Tile id array required to download files.")
            return JSONErrorResponse(_("Request Failed"), _(e))

    def get(self, request):
        if self.action == "tile_history":
            start = request.GET.get("start")
            end = request.GET.get("end")
            edits = (
                EditLog.objects.filter(provisional_userid=request.user.id)
                .filter(timestamp__range=[start, end])
                .order_by("tileinstanceid", "timestamp")
            )
            resourceinstanceids = [e["resourceinstanceid"] for e in edits.values("resourceinstanceid")]
            deleted_resource_edits = EditLog.objects.filter(resourceinstanceid__in=resourceinstanceids).filter(edittype="delete")
            deleted_resource_instances = [e["resourceinstanceid"] for e in deleted_resource_edits.values("resourceinstanceid")]
            summary = {}
            for edit in edits:
                if edit.tileinstanceid not in summary:
                    summary[edit.tileinstanceid] = {"pending": False, "tileid": edit.tileinstanceid}
                summary[edit.tileinstanceid]["lasttimestamp"] = edit.timestamp
                summary[edit.tileinstanceid]["lastedittype"] = edit.provisional_edittype
                summary[edit.tileinstanceid]["reviewer"] = ""
                summary[edit.tileinstanceid]["resourceinstanceid"] = edit.resourceinstanceid
                summary[edit.tileinstanceid]["resourcedisplayname"] = edit.resourcedisplayname
                summary[edit.tileinstanceid]["resourcemodelid"] = edit.resourceclassid
                summary[edit.tileinstanceid]["nodegroupid"] = edit.nodegroupid
                summary[edit.tileinstanceid]["resource_deleted"] = True if edit.resourceinstanceid in deleted_resource_instances else False
                if edit.provisional_edittype in ["accept edit", "delete edit"]:
                    summary[edit.tileinstanceid]["reviewer"] = edit.user_username

            chronological_summary = []
            resource_models = (
                models.GraphModel.objects.filter(isresource=True)
                .exclude(graphid=settings.SYSTEM_SETTINGS_RESOURCE_MODEL_ID)
                .values("iconclass", "color", "graphid", "name")
            )
            cards = models.CardModel.objects.all().values("name", "nodegroup_id")
            card_lookup = {str(card["nodegroup_id"]): card for card in cards}
            resource_model_lookup = {str(graph["graphid"]): graph for graph in resource_models}
            for k, v in summary.items():
                if v["lastedittype"] not in ["accept edit", "delete edit"]:
                    if models.TileModel.objects.filter(pk=k).exists():
                        tile = models.TileModel.objects.get(pk=k)
                        if tile.provisionaledits is not None and str(request.user.id) in tile.provisionaledits:
                            v["pending"] = True

                v["resourcemodel"] = resource_model_lookup[v["resourcemodelid"]]
                v["card"] = card_lookup[v["nodegroupid"]]
                if "graphid" in v["resourcemodel"]:
                    v["resourcemodel"].pop("graphid")
                if "nodegroup_id" in v["card"]:
                    v["card"].pop("nodegroup_id")
                chronological_summary.append(v)

            return JSONResponse(JSONSerializer().serialize(sorted(chronological_summary, key=lambda k: k["lasttimestamp"], reverse=True)))

        if self.action == "download_files":
            response = self.download_files(request)
            return response

# Move to util function
def get(id):
    try:
        uuid.UUID(id)
        return uuid.UUID(id), False
    except (ValueError, TypeError):
        return uuid.uuid4(), True


uuid.get_or_create = get


def update_system_settings_cache(tile):
    if str(tile.resourceinstance_id) == settings.RESOURCE_INSTANCE_ID:
        settings.update_from_db()
