# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE.  If not, see <http://www.gnu.org/licenses/>.
import os
import re
import subprocess
import sys
from datetime import timedelta
import json
import logging
from glob import glob
from hashlib import sha256, sha1
from shutil import move, rmtree
from string import Template
from tarfile import TarFile
from time import sleep, time
from xml.etree.cElementTree import parse as XMLparse
from xml.dom import NotFoundErr

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db.models import Count
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect, get_object_or_404
from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import (
    TemplateView, CreateView, UpdateView,
)
#import magic

from ..store_api import Store, NoStoreException
from braces.views import (
    LoginRequiredMixin, PermissionRequiredMixin,
)
from django_tables2 import SingleTableView

from vm.models import (
    InstanceTemplate, InterfaceTemplate, Instance, Lease, InstanceActivity, ExportedVM, OS_TYPES
)
from storage.models import Disk, DataStore

from ..forms import (
    TemplateForm, TemplateListSearchForm, AclUserOrGroupAddForm, LeaseForm, TemplateImportForm,
)
from ..tables import TemplateListTable, LeaseListTable

from .util import (
    AclUpdateView, FilterMixin,
    TransferOwnershipConfirmView, TransferOwnershipView,
    DeleteViewBase,
    GraphMixin
)

from dashboard.serializers import InstanceTemplateSerializer, LeaseSerializer, InstanceSerializer

from rest_framework.authentication import TokenAuthentication, BasicAuthentication
from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView
from rest_framework.parsers import JSONParser

try:
    # Python 2: "unicode" is built-in
    unicode
except NameError:
    unicode = str

    try:
            # Python 2: "unicode" is built-in
                unicode
    except NameError:
            unicode = str

logger = logging.getLogger(__name__)


class TemplateChoose(LoginRequiredMixin, TemplateView):

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/_modal.html']
        else:
            return ['dashboard/nojs-wrapper.html']

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateChoose, self).get_context_data(*args, **kwargs)
        templates = InstanceTemplate.get_objects_with_level("user",
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': True,
            'template': "dashboard/_template-choose.html",
            'templates': templates.all(),
        })
        return context

    def post(self, request, *args, **kwargs):
        if not request.user.has_perm('vm.create_template'):
            raise PermissionDenied()

        template = request.POST.get("parent")
        if template == "base_vm":
            return redirect(reverse("dashboard.views.template-create"))
        elif template == "import_vm":
            return redirect(reverse("dashboard.views.template-import"))
        elif template is None:
            messages.warning(request, _("Select an option to proceed."))
            return redirect(reverse("dashboard.views.template-choose"))
        else:
            template = get_object_or_404(InstanceTemplate, pk=template)

        if not template.has_level(request.user, "user"):
            raise PermissionDenied()

        instance = Instance.create_from_template(
            template=template, owner=request.user, is_base=True)

        return redirect(instance.get_absolute_url())


class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

    def get_template_names(self):
        if self.request.is_ajax():
            pass
        else:
            return ['dashboard/nojs-wrapper.html']

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateCreate, self).get_context_data(*args, **kwargs)

        num_leases = Lease.get_objects_with_level("operator",
                                                  self.request.user).count()
        can_create_leases = self.request.user.has_perm("create_leases")
        context.update({
            'box_title': _("Create a new base VM"),
            'template': "dashboard/_template-create.html",
            'show_lease_create': num_leases < 1 and can_create_leases
        })
        return context

    def get(self, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_base_template'):
            raise PermissionDenied()

        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_base_template'):
            raise PermissionDenied()

        form = self.form_class(request.POST, user=request.user)
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
        else:
            post = form.cleaned_data
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
            post.pop("parent")
            post['max_ram_size'] = post['ram_size']
            req_traits = post.pop("req_traits")
            tags = post.pop("tags")
            post['pw'] = User.objects.make_random_password()
            post['is_base'] = True

            inst = Instance.create(params=post, disks=[],
                                   networks=networks,
                                   tags=tags, req_traits=req_traits)

            return HttpResponseRedirect("%s#resources" %
                                        inst.get_absolute_url())

    def __create_networks(self, vlans, user):
        networks = []
        for v in vlans:
            if not v.has_level(user, "user"):
                raise PermissionDenied()
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


class TemplateImport(LoginRequiredMixin, TemplateView):
    form_class = TemplateImportForm

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/_modal.html']
        else:
            return ['dashboard/nojs-wrapper.html']

    def get(self, request, form=None, *args, **kwargs):
        if not request.user.has_perm('vm.import_template'):
            raise PermissionDenied()
        try:
            Store(request.user)
        except NoStoreException:
            raise PermissionDenied

        if form is None:
            form = self.form_class(user=request.user)
        context = self.get_context_data(**kwargs)
        context.update({
            'template': 'dashboard/template-import.html',
            'box_title': _('Import a VM to a template'),
            'form': form,
            'ajax_title': True,
        })
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        if not request.user.has_module_perms('vm.import_template'):
            raise PermissionDenied()
        try:
            store = Store(request.user)
        except NoStoreException:
            raise PermissionDenied()

        form = self.form_class(request.POST, user=request.user)
        if form.is_valid():
            datastore = DataStore.objects.filter(name=settings.EXPORT_DATASTORE).get()
            ova_path = form.cleaned_data["ova_path"]
            template_name = form.cleaned_data["template_name"]
            url, port = store.request_ssh_download(ova_path)
            ova_filename = re.split('[:/]', url)[-1]
            tmp_path = os.path.join(datastore.path, "tmp", ova_filename + str(time()))
            ova_file = os.path.join(tmp_path, ova_filename)
            try:
                os.mkdir(os.path.join(datastore.path, "tmp"))
            except FileExistsError:
                pass
            try:
                os.mkdir(tmp_path)
            except FileExistsError:
                pass
            if settings.STORE_SSH_MODE == "scp":
                cmdline = ['scp', '-B', '-P', str(port), url, ova_file]
                if settings.STORE_IDENTITY_FILE is not None and settings.STORE_IDENTITY_FILE != "":
                    cmdline.append("-i")
                    cmdline.append(settings.STORE_IDENTITY_FILE)
            elif settings.STORE_SSH_MODE == "rsync":
                cmdline = ["rsync", "-qLS", url, ova_file]
                cmdline.append("-e")
                if settings.STORE_IDENTITY_FILE is not None:
                    cmdline.append("ssh -i %s -p %s" % (settings.STORE_IDENTITY_FILE, str(port)))
                else:
                    cmdline.append("ssh -p %s" % str(port))
            else:
                logger.error("Invalid mode for disk export: %s" % settings.STORE_SSH_MODE)
                raise Exception("Invalid mode for disk export: %s" % settings.STORE_SSH_MODE)

            logger.debug("Calling file transfer with command line: %s" % str(cmdline))
            try:
                # let's try the file transfer 5 times, it may be an intermittent network issue
                for i in range(4, -1, -1):
                    proc = subprocess.Popen(cmdline)
                    while proc.poll() is None:
                        sleep(2)
                    if proc.returncode == 0:
                        break
                    else:
                        logger.error("Copy over ssh failed with return code: %s, will try %s more time(s)..." % (str(proc.returncode), str(i)))
                        if proc.stdout is not None:
                            logger.info(proc.stdout.read())
                        if proc.stdout is not None:
                            logger.error(proc.stderr.read())
                with TarFile.open(name=ova_file, mode="r") as tar:
                    if sys.version_info >= (3, 12):
                        tar.extractall(path=tmp_path, filter='data')
                    else:
                        for tarinfo in tar:
                            if tarinfo.name.startswith("..") or tarinfo.name.startswith("/") or tarinfo.name.find("/..") != -1:
                                raise Exception("import template: invalid path in tar file")
                            if tarinfo.isreg():
                                tar.extract(tarinfo, path=tmp_path, set_attrs=False)
                            elif tarinfo.isdir():
                                tar.extract(tarinfo, path=tmp_path, set_attrs=False)
                            else:
                                raise Exception("import template: invalid file type in tar file")
                os.unlink(ova_file)
                mf = glob(os.path.join(tmp_path, "*.mf"))
                if len(mf) > 1:
                    logger.error("import template: Invalid ova: multiple mf files!")
                    messages.error("import template: Invalid ova: multiple mf files!")
                    raise Exception("Invalid ova: multiple mf files")
                elif len(mf) == 1:
                    with open(os.path.join(tmp_path, mf[0]), 'r') as mf_file:
                        def compute_sha256(file_name):
                            hash_sha256 = sha256()
                            with open(file_name, "rb") as f:
                                for chunk in iter(lambda: f.read(4096), b""):
                                    hash_sha256.update(chunk)
                            return hash_sha256.hexdigest()

                        def compute_sha1(file_name):
                            hash_sha1 = sha1()
                            with open(file_name, "rb") as f:
                                for chunk in iter(lambda: f.read(4096), b""):
                                    hash_sha1.update(chunk)
                            return hash_sha1.hexdigest()

                        for line in mf_file:
                            if line.startswith("..") or line.startswith("/") or line.find("/..") != -1:
                                logger.error("import template: mf: invalid path in mf file!")
                                messages.error("import template: mf: invalid path in mf file!")
                                raise Exception("import template: mf: invalid path in mf file!")
                            if line.startswith("SHA1"):
                                line_split = line.split("=")
                                filename = line_split[0][4:].strip()[1:-1]
                                hash_value = line_split[1].strip()
                                if compute_sha1(os.path.join(tmp_path, filename)) != hash_value:
                                    logger.error("import template: mf: hash check failed!")
                                    messages.error("import template: mf: hash check failed!")
                                    raise Exception("import template: mf: hash check failed!")
                                else:
                                    logger.info("%s passed hash test" % filename)
                            elif line.startswith("SHA256"):
                                line_split = line.split("=")
                                filename = line_split[0][6:].strip()[1:-1]
                                hash_value = line_split[1].strip()
                                if compute_sha256(os.path.join(tmp_path, filename)) != hash_value:
                                    logger.error("import template: mf: hash check failed!")
                                    messages.error("import template: mf: hash check failed!")
                                else:
                                    logger.info("%s passed hash test" % filename)
                            else:
                                logger.error("import template: mf: Invalid hash algorythm!")
                                messages.error("import template: mf: Invalid hash algorythm!")
                    os.unlink(os.path.join(tmp_path, mf[0]))
                ovf = glob(os.path.join(tmp_path, "*.ovf"))
                if len(ovf) != 1:
                    logger.error("import template: Invalid ova: multiple ovf files!")
                    messages.error("import template: Invalid ova: multiple ovf files!")
                    raise Exception("Invalid ova: multiple ovf files")
                xml = XMLparse(ovf[0])
                xml_root = xml.getroot()

                files = {}
                disks = {}
                disks_circle = []

                xml_references = xml_root.findall("{http://schemas.dmtf.org/ovf/envelope/2}References")
                if len(xml_references) == 1:
                    for xml_reference in xml_references[0].findall("{http://schemas.dmtf.org/ovf/envelope/2}File"):
                        files[xml_reference.get("{http://schemas.dmtf.org/ovf/envelope/2}id")] = xml_reference.get("{http://schemas.dmtf.org/ovf/envelope/2}href")

                xml_disk_section = xml_root.findall("{http://schemas.dmtf.org/ovf/envelope/2}DiskSection")
                if len(xml_disk_section) == 1:
                    for disk in xml_disk_section[0].findall("{http://schemas.dmtf.org/ovf/envelope/2}Disk"):
                        disks[disk.get("{http://schemas.dmtf.org/ovf/envelope/2}diskId")] = {"name": files[disk.get("{http://schemas.dmtf.org/ovf/envelope/2}fileRef")], "type": disk.get("{http://schemas.dmtf.org/ovf/envelope/2}format") }

                xml_VirtualSystem = xml_root.findall("{http://schemas.dmtf.org/ovf/envelope/2}VirtualSystem")[0]
                ovf_os = xml_VirtualSystem.findall("{http://schemas.dmtf.org/ovf/envelope/2}OperatingSystemSection")[0].findall("{http://schemas.dmtf.org/ovf/envelope/2}Description")[0].text
                arch_id = xml_VirtualSystem.findall("{http://schemas.dmtf.org/ovf/envelope/2}OperatingSystemSection")[0].get("{http://schemas.dmtf.org/ovf/envelope/2}id")
                if arch_id == "102":
                    arch = "x86_64"
                elif arch_id == 0 or arch_id == 1:
                    arch = "i686"
                else:
                    os_type: str = OS_TYPES[int(arch_id)]
                    if os_type.endswith("64-Bit"):
                        arch = "x86_64"
                    else:
                        arch = "i686"
                try:
                    ovf_description = xml_VirtualSystem.findall("{http://schemas.dmtf.org/ovf/envelope/2}Description")[0].text
                except Exception as e:
                    logger.error("Couldn't load description from ovf: %s" % e)
                    ovf_description = ""

                xml_hardware = xml_VirtualSystem.findall("{http://schemas.dmtf.org/ovf/envelope/2}VirtualHardwareSection")[0]
                for item in xml_hardware.iter():
                    if item.tag == "{http://schemas.dmtf.org/ovf/envelope/2}Item":
                        resource_type = item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}ResourceType")[0].text
                        # CPU
                        if resource_type == "3":
                            cores = int(item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}VirtualQuantity")[0].text)
                            logger.info("import ovf: cores: %s" % cores)
                        # memory
                        if resource_type == "4":
                            memory = int(item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}VirtualQuantity")[0].text)
                            try:
                                unit = item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}VirtualQuantityUnits")[0].text.lower()
                            except:
                                try:
                                    unit = item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData}AllocationUnits")[0].text.lower()
                                except:
                                    raise Exception("No unit for memory.")
                            unit_split = unit.split("*")
                            unit_base = unit_split[0].strip()
                            if unit_base in ["bytes", "kilobytes", "megabytes", "gigayytes"]:
                                if unit_base == "kilobytes":
                                    memory = memory * 1000
                                elif unit_base == "megabytes":
                                    memory = memory * 1000 * 1000
                                elif unit_base == "gigabytes":
                                    memory = memory * 1000 * 1000 * 1000
                            else:
                                raise Exception("Invalid unit for memory.")
                            if len(unit_split) == 2:
                                unit_numbers = unit_split[1].strip().split("^")
                                memory = memory * (int(unit_numbers[0].strip()) ** int(unit_numbers[1].strip()))
                            memory = int(memory / 1024 / 1024)
                            logger.info("import ovf: memory: %s MiB" % memory)
                    elif item.tag == "{http://schemas.dmtf.org/ovf/envelope/2}StorageItem":
                        if item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_StorageAllocationSettingData.xsd}ResourceType")[0].text.lower() == str(17):
                            disk_no = item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_StorageAllocationSettingData.xsd}HostResource")[0].text.split("/")[-1]
                            disk_name = item.findall("{http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_StorageAllocationSettingData.xsd}Caption")[0].text
                            datastore = DataStore.objects.filter(name='default').get()
                            circle_disk = Disk.create(datastore=datastore, type="qcow2-norm", name=disk_name)
                            imported_path = os.path.join(tmp_path, disks[disk_no]["name"])
                            logger.debug("Calling qemu-img with command line: %s" % str(cmdline))
                            cmdline = ['ionice', '-c', 'idle',
                                        'qemu-img', 'convert',
                                        '-m', '4', '-O', 'qcow2',
                                        imported_path,
                                        os.path.join(datastore.path, circle_disk.filename)]
                            circle_disk.is_ready = True
                            circle_disk.save()
                            logger.debug("Calling qemu-img with command line: %s" % str(cmdline))
                            subprocess.check_output(cmdline)
                            disks_circle.append(circle_disk)

                template = InstanceTemplate(name=template_name,
                                            access_method='ssh',
                                            description=ovf_description,
                                            system=ovf_os,
                                            num_cores=cores,
                                            num_cores_max=cores,
                                            ram_size=memory,
                                            max_ram_size=memory,
                                            arch=arch,
                                            priority=0,
                                            owner=self.request.user,
                                            lease=form.cleaned_data["lease"],
                                            )
                template.save()
                for disk in disks_circle:
                    template.disks.add(disk)
                template.save()
                return redirect(template.get_absolute_url())
            except Exception as e:
                logger.error(e)
                raise
            finally:
                if os.path.exists(ova_path):
                    os.unlink(ova_path)
                if os.path.exists(tmp_path):
                    rmtree(tmp_path)
        else:
            return self.get(request, form, *args, **kwargs)


class TemplateREST(APIView):
    authentication_classes = [TokenAuthentication,BasicAuthentication]
    permission_classes = [IsAdminUser]

    def get(self, request, format=None):
        if request.query_params.get("name"):
            try:
                template = InstanceTemplate.objects.filter(name__istartswith=request.query_params.get('name')).get()
                serializer = InstanceTemplateSerializer(template, many=False)
                return JsonResponse(serializer.data, safe=False)
            except:
                return JsonResponse(state=411)
        templates = InstanceTemplate.objects.all()
        serializer = InstanceTemplateSerializer(templates, many=True)
        return JsonResponse(serializer.data, safe=False)

    def post(self, request, format=None):
        data = JSONParser().parse(request)
        serializer = InstanceTemplateSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)

class GetTemplateREST(APIView):
    authentication_classes = [TokenAuthentication,BasicAuthentication]
    permission_classes = [IsAdminUser]

    def get(self, request, pk, format=None):
        templates = InstanceTemplate.objects.get(pk=pk)
        serializer = InstanceTemplateSerializer(templates, many=False)
        return JsonResponse(serializer.data, safe=False)

    def delete(self, request, pk, format=None):
        return JsonResponse(status=400)


class InstanceFromTemplateREST(APIView):
    authentication_classes = [TokenAuthentication,BasicAuthentication]
    permission_classes = [IsAdminUser]

    def post(self, request, format=None):
        data = JSONParser().parse(request)
        user = User.objects.get(pk=request.user.pk)
        template = InstanceTemplate.objects.get(pk=data['template'])
        ikwargs = {
            'name': data['name'],
            'template': template,
            'owner': user,
        }
        amount = data.get("amount", 1)
        if 'num_cores' in data:
            ikwargs.update({'num_cores':data['num_cores']})
        
        if 'ram_size' in data:
            ikwargs.update({'ram_size':data['ram_size']})

        if 'priority' in data:
            ikwargs.update({'priority':data['priority']})

        if 'max_ram_size' in data:
            ikwargs.update({'max_ram_size':data['ram_size']})

        instances = Instance.mass_create_from_template(amount=amount,
                                                       **ikwargs)
        for i in instances:
            i.deploy._async(user=user)
        if amount == 1:
            serializer = InstanceSerializer(instances, many=True)
            return JsonResponse(serializer.data, status=201, safe=False)
        serializer = InstanceSerializer(instances, many=True)
        return JsonResponse(serializer.data, status=201, safe=False)

class InstanceFTforUsersREST(APIView):
    authentication_classes = [TokenAuthentication,BasicAuthentication]
    permission_classes = [IsAdminUser]

    def post(self, request, format=None):
        data = JSONParser().parse(request)
        template = InstanceTemplate.objects.get(pk=data['template'])
        ikwargs = {
            'name': data['name'],
            'users': data['users'],
            'template': template,
            'operator': data.get('operator', None),
            'admin': data.get('admin', None)
        }
        if 'num_cores' in data:
            ikwargs.update({'num_cores':data['num_cores']})
        
        if 'ram_size' in data:
            ikwargs.update({'ram_size':data['ram_size']})

        if 'priority' in data:
            ikwargs.update({'priority':data['priority']})

        if 'max_ram_size' in data:
            ikwargs.update({'max_ram_size':data['ram_size']})

        missing_users, instances = Instance.mass_create_for_users(**ikwargs)

        serializer = InstanceSerializer(instances, many=True)
        return JsonResponse(serializer.data, status=201)

class InstanceFTforUsersIdREST(APIView):
    authentication_classes = [TokenAuthentication,BasicAuthentication]
    permission_classes = [IsAdminUser]

    def post(self, request, format=None):
        data = JSONParser().parse(request)
        template = InstanceTemplate.objects.get(pk=int(data['template']))
        ikwargs = {
            'name': data['name'],
            'users': data['users'],
            'template': template,
            'operator': data.get('operator', None),
            'admin': data.get('admin', None)        
        }
        if 'num_cores' in data:
            ikwargs.update({'num_cores':data['num_cores']})
        
        if 'ram_size' in data:
            ikwargs.update({'ram_size':data['ram_size']})

        if 'priority' in data:
            ikwargs.update({'priority':data['priority']})

        if 'max_ram_size' in data:
            ikwargs.update({'max_ram_size':data['ram_size']})

        missing_users, instances = Instance.mass_create_for_users_id(**ikwargs)

        serializer = InstanceSerializer(instances, many=True)
        return JsonResponse(serializer.data, status=201, safe=False)


class LeaseREST(APIView):
    authentication_classes = [TokenAuthentication, BasicAuthentication]
    permission_classes = [IsAdminUser]

    def get(self, request, format=None):
        if request.query_params.get('name'):
            try:
                template = Lease.objects.filter(name__istartswith=request.query_params.get('name')).get()
                serializer = LeaseSerializer(template, many=False)
                return JsonResponse(serializer.data, safe=False)
            except:
                return JsonResponse({}, status=404)
        templates = Lease.objects.all()
        serializer = LeaseSerializer(templates, many=True)
        return JsonResponse(serializer.data, safe=False)

    def post(self, request, format=None):
        data = JSONParser().parse(request)
        serializer = LeaseSerializer(data=data)
        if serializer.is_valid():
            serializer.save()
            return JsonResponse(serializer.data, status=201)
        return JsonResponse(serializer.errors, status=400)


class GetLeaseREST(APIView):
    authentication_classes = [TokenAuthentication, BasicAuthentication]
    permission_classes = [IsAdminUser]

    def get(self, request, pk, format=None):
        lease = Lease.objects.get(pk=pk)
        serializer = LeaseSerializer(lease, many=False)
        return JsonResponse(serializer.data, safe=False)


class TemplateAclUpdateView(AclUpdateView):
    model = InstanceTemplate


class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
    template_name = "dashboard/template-list.html"
    model = InstanceTemplate
    table_class = TemplateListTable
    table_pagination = False

    allowed_filters = {
        'name': "name__icontains",
        'tags[]': "tags__name__in",
        'tags': "tags__name__in",  # for search string
        'owner': "owner__username",
        'ram': "ram_size",
        'ram_size': "ram_size",
        'cores': "num_cores",
        'num_cores': "num_cores",
        'access_method': "access_method__iexact",
    }

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateList, self).get_context_data(*args, **kwargs)
        user = self.request.user
        leases_w_operator = Lease.get_objects_with_level("operator", user)
        context['lease_table'] = LeaseListTable(
            leases_w_operator, request=self.request,
            template_name="django_tables2/table_no_page.html",
        )
        context['show_lease_table'] = (
            leases_w_operator.count() > 0 or
            user.has_perm("vm.create_leases")
        )

        context['search_form'] = self.search_form

        # templates without any instances
        # [t for t in InstanceTemplate.objects.all()
        #  if t.instance_set.count() < 1]
        never_instantiated = context['object_list'].annotate(
            instance_count=Count("instance_set")).filter(instance_count__lt=1)

        # templates without active virtual machines
        active_statuses = Instance.STATUS._db_values - set(["DESTROYED"])
        templates_wo_instances = context['object_list'].exclude(
            pk__in=InstanceTemplate.objects.filter(
                instance_set__status__in=active_statuses)
        ).exclude(pk__in=never_instantiated)

        def get_create_acts_younger_than(days):
            return InstanceActivity.objects.filter(
                activity_code="vm.Instance.create",
                finished__gt=timezone.now() - timedelta(days=days))

        # templates without active virtual machines
        # last machine started later than 90 days
        templates_wo_i_90 = templates_wo_instances.exclude(
            instance_set__activity_log__in=get_create_acts_younger_than(90))

        # templates without active virtual machines
        # last machine started later than 180 days
        templates_wo_i_180 = templates_wo_instances.exclude(
            instance_set__activity_log__in=get_create_acts_younger_than(180))

        context['unused_templates'] = {
            'never_instantiated': never_instantiated,
            'templates_wo_instances': templates_wo_instances,
            'templates_wo_instances_90': templates_wo_i_90,
            'templates_wo_instances_180': templates_wo_i_180,
        }

        return context

    def get(self, *args, **kwargs):
        self.search_form = TemplateListSearchForm(self.request.GET)
        self.search_form.full_clean()
        if self.request.is_ajax():
            templates = [{
                'icon': i.os_type,
                'system': i.system,
                'url': reverse("dashboard.views.template-detail",
                               kwargs={'pk': i.pk}),
                'name': i.name} for i in self.get_queryset()]
            return HttpResponse(
                json.dumps(templates),
                content_type="application/json",
            )
        else:
            return super(TemplateList, self).get(*args, **kwargs)

    def create_acl_queryset(self, model):
        queryset = super(TemplateList, self).create_acl_queryset(model)
        sql = ("SELECT count(*) FROM vm_instance WHERE "
               "vm_instance.template_id = vm_instancetemplate.id and "
               "vm_instance.destroyed_at is null and "
               "vm_instance.status = 'RUNNING'")
        queryset = queryset.extra(select={'running': sql})
        return queryset

    def get_queryset(self):
        logger.debug('TemplateList.get_queryset() called. User: %s', unicode(self.request.user))
        qs = self.create_acl_queryset(InstanceTemplate)
        self.create_fake_get()

        try:
            filters, excludes = self.get_queryset_filters()
            qs = qs.filter(**filters).exclude(**excludes).distinct()
        except ValueError:
            messages.error(self.request, _("Error during filtering."))

        return qs.select_related("lease", "owner", "owner__profile")


class TemplateDelete(DeleteViewBase):
    model = InstanceTemplate
    success_message = _("Template successfully deleted.")

    def get_success_url(self):
        return reverse("dashboard.views.template-list")

    def delete_obj(self, request, *args, **kwargs):
        object = self.get_object()
        object.destroy_disks()
        object.delete()


class TemplateDetail(LoginRequiredMixin, GraphMixin,
                     SuccessMessageMixin, UpdateView):
    model = InstanceTemplate
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
    success_message = _("Successfully modified template.")

    def get(self, request, *args, **kwargs):
        template = self.get_object()
        if not template.has_level(request.user, 'user'):
            raise PermissionDenied()
        if request.is_ajax():
            template = {
                'num_cores': template.num_cores,
                'ram_size': template.ram_size,
                'priority': template.priority,
                'arch': template.arch,
                'description': template.description,
                'system': template.system,
                'name': template.name,
                'disks': [{'pk': d.pk, 'name': d.name}
                          for d in template.disks.all()],
                'network': [
                    {'vlan_pk': i.vlan.pk, 'vlan': i.vlan.name,
                     'managed': i.managed}
                    for i in InterfaceTemplate.objects.filter(
                        template=self.get_object()).all()
                ]
            }
            return HttpResponse(json.dumps(template),
                                content_type="application/json")
        else:
            return super(TemplateDetail, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        obj = self.get_object()
        context = super(TemplateDetail, self).get_context_data(**kwargs)
        context['acl'] = AclUpdateView.get_acl_data(
            obj, self.request.user, 'dashboard.views.template-acl')
        context['disks'] = obj.disks.all()
        context['is_owner'] = obj.has_level(self.request.user, 'owner')
        context['aclform'] = AclUserOrGroupAddForm()
        context['parent'] = obj.parent
        context['show_graph'] = obj.has_level(self.request.user, 'operator')
        return context

    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

    def post(self, request, *args, **kwargs):
        template = self.get_object()
        if not template.has_level(request.user, 'owner'):
            raise PermissionDenied()
        return super(TemplateDetail, self).post(self, request, args, kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs


class DiskRemoveView(DeleteViewBase):
    model = Disk
    success_message = _("Disk successfully removed.")

    def get_queryset(self):
        qs = super(DiskRemoveView, self).get_queryset()
        return qs.exclude(template_set=None)

    def check_auth(self):
        disk = self.get_object()
        template = disk.template_set.get()
        if not template.has_level(self.request.user, 'owner'):
            raise PermissionDenied()

    def get_context_data(self, **kwargs):
        disk = self.get_object()
        template = disk.template_set.get()
        context = super(DiskRemoveView, self).get_context_data(**kwargs)
        context['title'] = _("Disk remove confirmation")
        context['text'] = _("Are you sure you want to remove "
                            "<strong>%(disk)s</strong> from "
                            "<strong>%(app)s</strong>?" % {'disk': disk,
                                                           'app': template}
                            )
        return context

    def delete_obj(self, request, *args, **kwargs):
        disk = self.get_object()
        template = disk.template_set.get()
        template.remove_disk(disk)
        disk.destroy()

    def get_success_url(self):
        return self.request.POST.get("next") or "/"


class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
                  SuccessMessageMixin, CreateView):
    model = Lease
    form_class = LeaseForm
    permission_required = 'vm.create_leases'
    template_name = "dashboard/lease-create.html"
    success_message = _("Successfully created a new lease.")

    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")

    def form_valid(self, form):
        retval = super(LeaseCreate, self).form_valid(form)
        self.object.set_level(self.request.user, "owner")
        return retval


class LeaseAclUpdateView(AclUpdateView):
    model = Lease


class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
    model = Lease
    form_class = LeaseForm
    template_name = "dashboard/lease-edit.html"
    success_message = _("Successfully modified lease.")

    def get_context_data(self, *args, **kwargs):
        obj = self.get_object()
        context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
        context['acl'] = AclUpdateView.get_acl_data(
            obj, self.request.user, 'dashboard.views.lease-acl')
        return context

    def get_success_url(self):
        return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)

    def get(self, request, *args, **kwargs):
        if not self.get_object().has_level(request.user, "owner"):
            message = _("Only the owners can modify the selected lease.")
            messages.warning(request, message)
            return redirect(reverse_lazy("dashboard.views.template-list"))
        return super(LeaseDetail, self).get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        if not self.get_object().has_level(request.user, "owner"):
            raise PermissionDenied()

        return super(LeaseDetail, self).post(request, *args, **kwargs)


class LeaseDelete(DeleteViewBase):
    model = Lease
    success_message = _("Lease successfully deleted.")

    def get_success_url(self):
        return reverse("dashboard.views.template-list")

    def get_context_data(self, *args, **kwargs):
        c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
        lease = self.get_object()
        templates = lease.instancetemplate_set
        if templates.count() > 0:
            text = _("You can't delete this lease because some templates "
                     "are still using it, modify these to proceed: ")

            c['text'] = text + ", ".join("<strong>%s (#%d)</strong>"
                                         "" % (o.name, o.pk)
                                         for o in templates.all())
            c['disable_submit'] = True
        return c

    def delete_obj(self, request, *args, **kwargs):
        object = self.get_object()
        if object.instancetemplate_set.count() > 0:
            raise SuspiciousOperation()
        object.delete()


class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
    template = "dashboard/confirm/transfer-template-ownership.html"
    model = InstanceTemplate


class TransferTemplateOwnershipView(TransferOwnershipView):
    confirm_view = TransferTemplateOwnershipConfirmView
    model = InstanceTemplate
    notification_msg = ugettext_noop(
        '%(owner)s offered you to take the ownership of '
        'his/her template called %(instance)s. '
        '<a href="%(token)s" '
        'class="btn btn-success btn-small">Accept</a>')
    token_url = 'dashboard.views.template-transfer-ownership-confirm'
    template = "dashboard/template-tx-owner.html"


class ExportedVMDelete(DeleteViewBase):
    model = ExportedVM
    success_message = _("Exported VM successfully deleted.")

    def get_success_url(self):
        #return reverse("dashboard.views.vm-list")
        return reverse_lazy("dashboard.views.detail", kwargs={'pk': self.vm_pk})

    def check_auth(self):
        if not self.get_object().vm.has_level(self.request.user, self.level):
            raise PermissionDenied()

    def delete_obj(self, request, *args, **kwargs):
        self.get_object().delete()