from datetime import timedelta

import django.conf
import logging
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel
from netaddr import EUI

from . import tasks
from firewall.models import Vlan, Host
from manager import manager, scheduler
from storage.models import Disk


logger = logging.getLogger(__name__)
pwgen = User.objects.make_random_password
ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS
ACCESS_METHODS = [(k, ap[0]) for k, ap in ACCESS_PROTOCOLS.iteritems()]


class BaseResourceConfigModel(models.Model):

    """Abstract base class for models with base resource configuration
       parameters.
    """
    num_cores = models.IntegerField(help_text=_('Number of CPU cores.'))
    ram_size = models.IntegerField(help_text=_('Mebibytes of memory.'))
    max_ram_size = models.IntegerField(help_text=_('Upper memory size limit '
                                                   'for balloning.'))
    arch = models.CharField(max_length=10, verbose_name=_('architecture'))
    priority = models.IntegerField(help_text=_('instance priority'))
    boot_menu = models.BooleanField(default=False)
    raw_data = models.TextField(blank=True, null=True)

    class Meta:
        abstract = True


class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):

    """Pre-created, named base resource configurations.
    """
    name = models.CharField(max_length=50, unique=True,
                            verbose_name=_('name'))

    def __unicode__(self):
        return self.name


class Node(TimeStampedModel):

    """A VM host machine.
    """
    name = models.CharField(max_length=50, unique=True,
                            verbose_name=_('name'))
    num_cores = models.IntegerField(help_text=_('Number of CPU cores.'))
    ram_size = models.IntegerField(help_text=_('Mebibytes of memory.'))
    priority = models.IntegerField(help_text=_('node usage priority'))
    host = models.ForeignKey(Host)
    enabled = models.BooleanField(default=False,
                                  help_text=_('Indicates whether the node can '
                                              'be used for hosting.'))

    class Meta:
        permissions = ()

    @property
    def online(self):
        """Indicates whether the node is connected and functional.
        """
        pass  # TODO implement check


class NodeActivity(TimeStampedModel):
    activity_code = models.CharField(max_length=100)
    task_uuid = models.CharField(blank=True, max_length=50, null=True,
                                 unique=True)
    node = models.ForeignKey(Node, related_name='activity_log')
    user = models.ForeignKey(User, blank=True, null=True)
    started = models.DateTimeField(blank=True, null=True)
    finished = models.DateTimeField(blank=True, null=True)
    result = models.TextField(blank=True, null=True)
    status = models.CharField(default='PENDING', max_length=50)


class Lease(models.Model):

    """Lease times for VM instances.

    Specifies a time duration until suspension and deletion of a VM
    instance.
    """
    name = models.CharField(max_length=100, unique=True,
                            verbose_name=_('name'))
    suspend_interval_seconds = models.IntegerField()
    delete_interval_seconds = models.IntegerField()

    class Meta:
        ordering = ['name', ]

    @property
    def suspend_interval(self):
        return timedelta(seconds=self.suspend_interval_seconds)

    @suspend_interval.setter
    def suspend_interval(self, value):
        self.suspend_interval_seconds = value.seconds

    @property
    def delete_interval(self):
        return timedelta(seconds=self.delete_interval_seconds)

    @delete_interval.setter
    def delete_interval(self, value):
        self.delete_interval_seconds = value.seconds


class InstanceTemplate(BaseResourceConfigModel, TimeStampedModel):

    """Virtual machine template.

    Every template has:
      * a name and a description
      * an optional parent template
      * state of the template
      * an OS name/description
      * a method of access to the system
      * default values of base resource configuration
      * list of attached images
      * set of interfaces
      * lease times (suspension & deletion)
      * time of creation and last modification
    """
    STATES = [('NEW', _('new')),  # template has just been created
              ('SAVING', _('saving')),  # changes are being saved
              ('READY', _('ready'))]  # template is ready for instantiation
    name = models.CharField(max_length=100, unique=True,
                            verbose_name=_('name'))
    description = models.TextField(verbose_name=_('description'),
                                   blank=True)
    parent = models.ForeignKey('self', null=True, blank=True,
                               verbose_name=_('parent template'))
    system = models.TextField(verbose_name=_('operating system'),
                              blank=True,
                              help_text=(_('Name of operating system in '
                                           'format like "%s".') %
                                         'Ubuntu 12.04 LTS Desktop amd64'))
    access_method = models.CharField(max_length=10, choices=ACCESS_METHODS,
                                     verbose_name=_('access method'))
    state = models.CharField(max_length=10, choices=STATES,
                             default='NEW')
    disks = models.ManyToManyField(Disk, verbose_name=_('disks'),
                                   related_name='template_set')
    lease = models.ForeignKey(Lease, related_name='template_set')

    class Meta:
        ordering = ['name', ]
        permissions = ()
        verbose_name = _('template')
        verbose_name_plural = _('templates')

    def __unicode__(self):
        return self.name

    def running_instances(self):
        """Returns the number of running instances of the template.
        """
        return self.instance_set.filter(state='RUNNING').count()

    @property
    def os_type(self):
        """Get the type of the template's operating system.
        """
        if self.access_method == 'rdp':
            return 'win'
        else:
            return 'linux'


class InterfaceTemplate(models.Model):

    """Network interface template for an instance template.

    If the interface is managed, a host will be created for it.
    """
    vlan = models.ForeignKey(Vlan)
    managed = models.BooleanField(default=True)
    template = models.ForeignKey(InstanceTemplate,
                                 related_name='interface_set')

    class Meta:
        permissions = ()
        verbose_name = _('interface template')
        verbose_name_plural = _('interface templates')


class Instance(BaseResourceConfigModel, TimeStampedModel):

    """Virtual machine instance.

    Every instance has:
      * a name and a description
      * an optional parent template
      * associated share
      * a generated password for login authentication
      * time of deletion and time of suspension
      * lease times (suspension & deletion)
      * last boot timestamp
      * host node
      * current state (libvirt domain state)
      * time of creation and last modification
      * base resource configuration values
      * owner and privilege information
    """
    STATES = [('NOSTATE', _('nostate')),
              ('RUNNING', _('running')),
              ('BLOCKED', _('blocked')),
              ('PAUSED', _('paused')),
              ('SHUTDOWN', _('shutdown')),
              ('SHUTOFF', _('shutoff')),
              ('CRASHED', _('crashed')),
              ('PMSUSPENDED', _('pmsuspended'))]  # libvirt domain states
    name = models.CharField(blank=True, max_length=100, verbose_name=_('name'))
    description = models.TextField(blank=True, verbose_name=_('description'))
    template = models.ForeignKey(InstanceTemplate, blank=True, null=True,
                                 related_name='instance_set',
                                 verbose_name=_('template'))
    pw = models.CharField(help_text=_('Original password of instance'),
                          max_length=20, verbose_name=_('password'))
    time_of_suspend = models.DateTimeField(blank=True, default=None, null=True,
                                           verbose_name=_('time of suspend'))
    time_of_delete = models.DateTimeField(blank=True, default=None, null=True,
                                          verbose_name=_('time of delete'))
    active_since = models.DateTimeField(blank=True, null=True,
                                        help_text=_('Time stamp of successful '
                                                    'boot report.'),
                                        verbose_name=_('active since'))
    node = models.ForeignKey(Node, blank=True, null=True,
                             related_name='instance_set',
                             verbose_name=_('host nose'))
    state = models.CharField(choices=STATES, default='NOSTATE', max_length=20)
    disks = models.ManyToManyField(Disk, related_name='instance_set',
                                   verbose_name=_('disks'))
    lease = models.ForeignKey(Lease)
    access_method = models.CharField(max_length=10, choices=ACCESS_METHODS,
                                     verbose_name=_('access method'))
    vnc_port = models.IntegerField()
    owner = models.ForeignKey(User)

    class Meta:
        ordering = ['pk', ]
        permissions = ()
        verbose_name = _('instance')
        verbose_name_plural = _('instances')

    def __unicode__(self):
        return self.name

    @classmethod
    def create_from_template(cls, template, owner, **kwargs):
        """Create a new instance based on an InstanceTemplate.

        Can also specify parameters as keyword arguments which should override
        template settings.
        """
        # prepare parameters
        kwargs['template'] = template
        kwargs['owner'] = owner
        kwargs.setdefault('name', template.name)
        kwargs.setdefault('description', template.description)
        kwargs.setdefault('pw', pwgen())
        kwargs.setdefault('num_cores', template.num_cores)
        kwargs.setdefault('ram_size', template.ram_size)
        kwargs.setdefault('max_ram_size', template.max_ram_size)
        kwargs.setdefault('arch', template.arch)
        kwargs.setdefault('priority', template.priority)
        kwargs.setdefault('boot_menu', template.boot_menu)
        kwargs.setdefault('raw_data', template.raw_data)
        kwargs.setdefault('lease', template.lease)
        kwargs.setdefault('access_method', template.access_method)
        # create instance and do additional setup
        inst = cls(**kwargs)
        for disk in template.disks:
            inst.disks.add(disk.get_exclusive())
        # save instance
        inst.save()
        # create related entities
        for iftmpl in template.interface_set.all():
            i = Interface.create_from_template(instance=inst, template=iftmpl)
            if i.host:
                i.host.enable_net()
                port, proto = ACCESS_PROTOCOLS[i.access_method][1:3]
                i.host.add_port(proto, i.get_port(), port)

        return inst

    @models.permalink
    def get_absolute_url(self):
        # TODO is this obsolete?
        return ('one.views.vm_show', None, {'iid': self.id})

    @property
    def vm_name(self):
        """Name of the VM instance.

        This is a unique identifier as opposed to the 'name' attribute, which
        is just for display.
        """
        return 'cloud-' + str(self.id)

    @property
    def primary_host(self):
        interfaces = self.interface_set.select_related('host')
        hosts = [i.host for i in interfaces if i.host]
        if not hosts:
            return None
        hs = [h for h in hosts if h.ipv6]
        if hs:
            return hs[0]
        hs = [h for h in hosts if not h.shared_ip]
        if hs:
            return hs[0]
        return hosts[0]

    @property
    def ipv4(self):
        """Primary IPv4 address of the instance.
        """
        return self.primary_host.ipv4 if self.primary_host else None

    @property
    def ipv6(self):
        """Primary IPv6 address of the instance.
        """
        return self.primary_host.ipv6 if self.primary_host else None

    @property
    def mac(self):
        """Primary MAC address of the instance.
        """
        return self.primary_host.mac if self.primary_host else None

    @property
    def uptime(self):
        """Uptime of the instance.
        """
        if self.active_since:
            return timezone.now() - self.active_since
        else:
            return timedelta()  # zero

    def get_age(self):
        """Deprecated. Use uptime instead.

        Get age of VM in seconds.
        """
        return self.uptime.seconds

    @property
    def waiting(self):
        """Indicates whether the instance's waiting for an operation to finish.
        """
        return self.activity_log.filter(finished__isnull=True).exists()

    def get_connect_port(self, use_ipv6=False):
        """Get public port number for default access method.
        """
        port, proto = ACCESS_PROTOCOLS[self.access_method][1:3]
        if self.primary_host:
            endpoints = self.primary_host.get_public_endpoints(port, proto)
            endpoint = endpoints['ipv6'] if use_ipv6 else endpoints['ipv4']
            return endpoint[1] if endpoint else None
        else:
            return None

    def get_connect_host(self, use_ipv6=False):
        """Get public hostname.
        """
        if not self.firewall_host:
            return _('None')
        proto = 'ipv6' if use_ipv6 else 'ipv4'
        return self.firewall_host.get_hostname(proto=proto)

    def get_connect_uri(self, use_ipv6=False):
        """Get access parameters in URI format.
        """
        try:
            port = self.get_connect_port(use_ipv6=use_ipv6)
            host = self.get_connect_host(use_ipv6=use_ipv6)
            proto = self.access_method
            if proto == 'ssh':
                proto = 'sshterm'
            return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
                    {'port': port, 'proto': proto, 'pw': self.pw,
                     'host': host})
        except:
            return

    def get_vm_desc(self):
        return {
            'name': self.vm_name,
            'vcpu': self.num_cores,
            'memory': self.ram_size,
            'memory_max': self.max_ram_size,
            'cpu_share': self.priority,
            'arch': self.arch,
            'boot_menu': self.boot_menu,
            'network_list': [n.get_vmnetwork_desc()
                             for n in self.interface_set.all()],
            'disk_list': [d.get_vmdisk_desc() for d in self.disks.all()],
            'graphics': {
                'type': 'vnc',
                'listen': '0.0.0.0',
                'passwd': '',
                'port': self.get_vnc_port()
            },
            'raw_data': self.raw_data
        }

    def deploy_async(self, user=None):
        """ Launch celery task to handle the job asynchronously.
        """
        manager.deploy.apply_async(self, user)

    def deploy(self, user=None, task_uuid=None):
        """ Deploy new virtual machine with network
        1. Schedule
        """
        act = InstanceActivity(activity_code='vm.Instance.deploy',
                               instance=self, user=user,
                               started=timezone.now(), task_uuid=task_uuid)
        act.save()

        # Schedule
        act.update_state("PENDING")
        self.node = scheduler.get_node()
        self.save()

        # Create virtual images
        act.update_state("PREPARING DISKS")
        for disk in self.disks.all():
            disk.deploy()

        # Deploy VM on remote machine
        act.update_state("DEPLOYING VM")
        tasks.create.apply_async(self.get_vm_desc,
                                 queue=self.node + ".vm").get()

        # Estabilish network connection (vmdriver)
        act.update_state("DEPLOYING NET")
        for net in self.interface_set.all():
            net.deploy()

        # Resume vm
        act.update_state("BOOTING")
        tasks.resume.apply_async(self.vm_name, queue=self.node + ".vm").get()

        act.finish(result='SUCCESS')

    def stop(self):
        # TODO implement
        pass

    def resume(self):
        # TODO implement
        pass

    def poweroff(self):
        # TODO implement
        pass

    def restart(self):
        # TODO implement
        pass

    def renew(self, which='both'):
        """Renew virtual machine instance leases.
        """
        if which not in ['suspend', 'delete', 'both']:
            raise ValueError('No such expiration type.')
        if which in ['suspend', 'both']:
            self.time_of_suspend = timezone.now() + self.lease.suspend_interval
        if which in ['delete', 'both']:
            self.time_of_delete = timezone.now() + self.lease.delete_interval
        self.save()

    def save_as(self):
        """Save image and shut down."""
        imgname = "template-%d-%d" % (self.template.id, self.id)
        from .tasks import SaveAsTask
        SaveAsTask.delay(one_id=self.one_id, new_img=imgname)
        self._change_state("SHUTDOWN")
        self.save()
        t = self.template
        t.state = 'SAVING'
        t.save()

    def check_if_is_save_as_done(self):
        if self.state != 'DONE':
            return False
        Disk.update(delete=False)
        imgname = "template-%d-%d" % (self.template.id, self.id)
        disks = Disk.objects.filter(name=imgname)
        if len(disks) != 1:
            return False
        self.template.disk_id = disks[0].id
        self.template.state = 'READY'
        self.template.save()
        self.firewall_host_delete()
        return True


@receiver(pre_delete, sender=Instance, dispatch_uid='delete_instance_pre')
def delete_instance_pre(sender, instance, using, **kwargs):
    # TODO implement
    pass


class InstanceActivity(TimeStampedModel):
    activity_code = models.CharField(max_length=100)
    task_uuid = models.CharField(blank=True, max_length=50, null=True,
                                 unique=True)
    instance = models.ForeignKey(Instance, related_name='activity_log')
    user = models.ForeignKey(User, blank=True, null=True)
    started = models.DateTimeField(blank=True, null=True)
    finished = models.DateTimeField(blank=True, null=True)
    result = models.TextField(blank=True, null=True)
    state = models.CharField(default='PENDING', max_length=50)

    def update_state(self, new_state):
        self.state = new_state
        self.save()

    def finish(self, result=None):
        if not self.finished:
            self.finished = timezone.now()
            self.result = result
            self.save()


class Interface(models.Model):

    """Network interface for an instance.
    """
    vlan = models.ForeignKey(Vlan, related_name="vm_interface")
    host = models.ForeignKey(Host, blank=True, null=True)
    instance = models.ForeignKey(Instance, related_name='interface_set')

    @property
    def mac(self):
        try:
            return self.host.mac
        except:
            return Interface.generate_mac(self.instance, self.vlan)

    @classmethod
    def generate_mac(cls, instance, vlan):
        """Generate MAC address for a VM instance on a VLAN.
        """
        # MAC 02:XX:XX:XX:XX:XX
        #        \________/\__/
        #           VM ID   VLAN ID
        i = instance.id & 0xfffffff
        v = vlan.vid & 0xfff
        m = (0x02 << 40) | (i << 12) | v
        return EUI(m)

    def get_vmnetwork_desc(self):
        return {
            'name': 'cloud-' + self.instance.id + '-' + self.vlan.vid,
            'bridge': 'cloud',
            'mac': self.mac,
            'ipv4': self.host.ipv4 if self.host is not None else None,
            'ipv6': self.host.ipv6 if self.host is not None else None,
            'vlan': self.vlan.vid,
            'managed': self.host is not None
        }

    @classmethod
    def create_from_template(cls, instance, template):
        """Create a new interface for an instance based on an
           InterfaceTemplate.
        """
        host = (Host(vlan=template.vlan, mac=cls.generate_mac(instance,
                                                              template.vlan))
                if template.managed else None)
        iface = cls(vlan=template.vlan, host=host, instance=instance)
        iface.save()
        return iface