import traceback
from uuid import UUID
from threading import Thread
from django.conf import settings
from django.db import transaction
from django.db.models import Max
from django.utils.timezone import now
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete
from .models import Cloud, Image, Instance, Volume, Mount, InstanceOperation, Group, GroupOperation
from .models import INSTANCE_STATUS, INSTANCE_OPERATION, OPERATION_STATUS, VOLUME_STATUS
from . import utils

#TODO use singleton

from django.dispatch import Signal
materialized = Signal(providing_args=["instance","name"])
tidied = Signal(providing_args=["instance","name"])
selected = Signal(providing_args=["instance","name"])
from .models import bootstraped, monitored, executed, destroyed

@receiver(materialized)
@receiver(destroyed)
@receiver(monitored)
@receiver(tidied)
@receiver(selected)
@receiver(executed)
@receiver(bootstraped)
def log(sender,instance,name,**kwargs):
    print('SIGNAL INFO:{}/{}/{}'.format(sender._meta.app_label, sender._meta.verbose_name, instance), name)

@receiver(post_save, sender=Cloud)
def bootstrap_cloud(sender,instance,**kwargs):
    if kwargs['created']:
        instance.import_image()
        instance.import_template()
        (pubkey,prikey)=utils.gen_ssh_key()
        pubkey+=' '+instance._key_name
        instance.instance_credential_private_key=prikey
        instance.driver.keypairs.create(name=instance._key_name, public_key=pubkey)
        instance.save()
        instance.bootstrap()

@receiver(pre_delete, sender=Cloud)
def cleanup_cloud(sender,instance,**kwargs):
    instance.driver.keypairs.delete(instance._key_name)

@receiver(post_save, sender=Image)
def clone_image(sender,instance,**kwargs):
    if not instance.parent: return
    if instance.parent.access_id != instance.access_id:
        raise Exception('Must keep access_id same with parent')
    if instance.parent.cloud != instance.cloud:
        raise Exception('Must keep cloud same with parent')
    if instance.min_ram<instance.parent.min_ram:
        sender.objects.filter(pk=instance.pk).update(min_ram=instance.parent.min_ram)
    if instance.min_disk<instance.parent.min_disk:
        sender.objects.filter(pk=instance.pk).update(min_disk=instance.parent.min_disk)

@receiver(pre_delete, sender=Image)
def destroy_image(sender,instance,**kwargs):
    if not instance.protected:
        if sender.objects.filter(access_id=instance.access_id).count()==1:# avoid to delete shared cloud images
            instance.cloud.driver.images.delete(instance.access_id)

# actions relies on status must be registered to the monitored signal first.
@receiver(materialized, sender=Instance)
@receiver(post_save, sender=Mount)
@receiver(tidied, sender=InstanceOperation)
@receiver(executed, sender=InstanceOperation)
def monitor_instance(sender, instance, **kwargs):
    if sender==Mount:
        if not kwargs['created'] or instance.ready: return
        instance=instance.instance
    if sender==InstanceOperation:
        if instance.status==OPERATION_STATUS.waiting.value: return
        instance=instance.target
    if not instance.ready: return
    Thread(target=instance.monitor).start()

@receiver(post_save, sender=Instance)
def materialize_instance(sender, instance, **kwargs):
    if not kwargs['created'] or instance.ready: return
    instance.built_time=now()
    instance.save()
    instance.update_remedy_script(instance.template.remedy_script+'\n'+instance.image.remedy_script)
    def materialize(instance=instance):
        remark = settings.PACKONE_LABEL+'.'+instance.cloud.name+';'
        if instance.remark: remark+=instance.remark
        ins=instance.cloud.driver.instances.create(
            instance.image.access_id,
            instance.template.access_id,
            remark
        )
        instance=sender.objects.get(pk=instance.pk)
        instance.uuid=UUID(ins.id.replace('-', ''), version=4)
        instance.built_time=ins.created
        instance.ipv4=ins.addresses['provider'][0]['addr']
        instance.save()
        hosts='###instance###\n'+instance.hosts_record
        if instance.cloud.hosts: hosts=hosts+'\n###cloud###\n'+instance.cloud.hosts
        instance.update_remedy_script(utils.remedy_script_hosts_add(hosts, overwrite=True),heading=True)
        instance.set_password()
        instance.set_public_key()
        instance.remedy(manual=False)
        materialized.send(sender=sender, instance=instance, name='materialized')
    Thread(target=materialize).start()

@receiver(pre_save, sender=Instance)
def update_instance_hostname(sender, instance, **kwargs):
    if not instance.pk:
        if not instance.hostname:
            instance.hostname=instance.image.hostname
        instance.remedy_script_todo+='\n'+utils.remedy_script_hostname(instance.hostname)
        return
    old=sender.objects.get(pk=instance.id)
    if old.hostname!=instance.hostname:
        instance.remedy(
            script=utils.remedy_script_hostname(instance.hostname),
            manual=False
        )

@receiver(pre_delete, sender=Instance)
def destroy_instance(sender,instance,**kwargs):
    #to aviold repeated deletion
    for instance in sender.objects.select_for_update().filter(pk=instance.pk):
        def destroy():
            if not instance.ready:
                print('WARNNING: delete instance under building')
            else:
                try:
                    instance.cloud.driver.instances.force_delete(str(instance.uuid))
                except Exception as e:#TODO may spam the log
                    instance.pk=None
                    instance.save()
                    traceback.print_exc()
                    return
            destroyed.send(sender=sender, instance=instance, name='destroyed')
        transaction.on_commit(Thread(target=destroy).start)

@receiver(post_save, sender=Volume)
def materialize_volume(sender, instance, **kwargs):
    if not kwargs['created'] or instance.ready: return
    instance.built_time=now()
    instance.save()
    @transaction.atomic
    def materialize(volume=instance):
        volume=sender.objects.select_for_update().get(pk=volume.pk)
        remark = settings.PACKONE_LABEL+'.'+volume.cloud.name+';'
        if volume.remark: remark+=volume.remark
        info=volume.cloud.driver.volumes.create(
            volume.capacity,
            remark=remark
        )
        volume.uuid=UUID(info.id.replace('-', ''), version=4)
        volume.built_time=now()
        volume.status=VOLUME_STATUS.available.value
        volume.save()
        materialized.send(sender=sender, instance=volume, name='materialized')
    transaction.on_commit(Thread(target=materialize).start)

@receiver(pre_delete, sender=Volume)
def destroy_volume(sender,instance,**kwargs):
    #to aviold repeated deletion
    for volume in sender.objects.select_for_update().filter(pk=instance.pk):
        def destroy():
            if not volume.ready:
                print('WARNNING: delete volume under building')
            else:
                try:
                    volume.cloud.driver.volumes.delete(
                        str(volume.uuid)
                    )
                except Exception as e:#TODO may spam the log
                    volume.pk=None
                    volume.save()
                    traceback.print_exc()
                    return
            destroyed.send(sender=sender, instance=volume, name='destroyed')
        transaction.on_commit(Thread(target=destroy).start)

@receiver(monitored, sender=Instance)
@receiver(materialized, sender=Volume)
@transaction.atomic
def mount(sender, instance, **kwargs):
    if instance.deleting: return
    instance=sender.objects.select_for_update().get(pk=instance.pk)
    if not instance.mountable: return
    mounts=instance.mount_set.select_for_update().filter(
        completed_time=None,
        volume__status = VOLUME_STATUS.available.value
    ) if sender==Instance else Mount.objects.select_for_update().filter(completed_time=None, volume=instance)
    if not mounts.exists(): return
    @transaction.atomic
    def materialize(mount):
        mount=Mount.objects.select_related('volume').select_for_update().get(pk=mount.pk)
        vol=mount.volume.cloud.driver.volumes.mount(
            str(mount.volume.uuid),
            str(mount.instance.uuid)
        )
        mount.dev=vol.attachments[0]['device']#TODO allow multiple mounts for the same volume
        mount.completed_time=now()
        mount.save()
        mount.instance.update_remedy_script(
            utils.remedy_script_mount_add(mount),
            heading=True
        )
        mount.volume.status=VOLUME_STATUS.mounted.value
        mount.volume.save()
        materialized.send(sender=Mount, instance=mount, name='materialized')
    for mount in mounts:
        if not mount.instance.mountable: continue
        mount.completed_time=now()
        mount.save()
        Thread(target=materialize,args=(mount,)).start()

@receiver(pre_delete, sender=Mount)
def umount(sender,instance,**kwargs):
    #to aviold repeated deletion
    for mount in sender.objects.select_for_update().filter(pk=instance.pk):
        @transaction.atomic
        def destroy(mount=mount):
            volume=Volume.objects.select_for_update().get(pk=mount.volume.pk)
            if not mount.ready:
                print('WARNNING: delete mount under building')
            else:
                try:
                    mount.volume.cloud.driver.volumes.unmount(
                        str(mount.volume.uuid),
                        str(mount.instance.uuid)
                    )
                except Exception as e:
                    mount.pk=None
                    mount.save()
                    traceback.print_exc()
                    return
                volume.status=VOLUME_STATUS.available.value
                volume.save()
                mount.instance.update_remedy_script(utils.remedy_script_mount_remove(mount))
            destroyed.send(sender=sender, instance=mount, name='destroyed')
        transaction.on_commit(Thread(target=destroy).start)

@receiver(materialized, sender=Instance)#TODO instance may created before be added to group
@receiver(materialized, sender=Mount)
@transaction.atomic
def materialize_group(sender,instance,**kwargs):
    if sender==Mount: instance=instance.instance
    elif instance.mount_set.all().exists(): return
    for group in instance.group_set.select_for_update():
        if group.ready: continue
        if sender==Mount and group.mounts.filter(dev=None).exists(): continue
        if sender==Instance and group.instances.filter(uuid=None).exists(): continue
        group.hosts = '###group {}###\n'.format(group.long_id)+'\n'.join([ins.hosts_record for ins in group.instances.all()])
        group.built_time=now()
        group.save()
        for ins in group.instances.all():
            ins.remedy(manual=False)
        GroupOperation(
            operation=INSTANCE_OPERATION.start.value,
            target=group,
            status=OPERATION_STATUS.running.value,
            ignore_error=True
        ).save()
        group.remedy(utils.remedy_script_hosts_add(group.hosts),manual=False)
        materialized.send(sender=Group, instance=group, name='materialized')

@receiver(destroyed, sender=Instance)
@transaction.atomic
def destroy_group(sender,instance,**kwargs):
    for group in Group.objects.select_for_update().filter(
        deleting=True,
    ):
        if not group.instances.all().exists():
            destroyed.send(sender=Group, instance=group, name='destroyed')
            group.delete()

@receiver(monitored, sender=Instance)
@receiver(tidied, sender=GroupOperation)
@receiver(executed, sender=GroupOperation)
def monitor_group(sender, instance, **kwargs):
    if sender==Instance:
        if instance.deleting: return
        for group in instance.group_set.all():
            status=group.instances.all().aggregate(Max('status'))['status__max']#TODO use join
            Group.objects.filter(pk=group.pk).update(status=status)
            group.refresh_from_db()
            monitored.send(sender=Group, instance=group, name='monitored')
    else:
        if instance.status==OPERATION_STATUS.waiting.value: return
        instance.target.monitor()

@receiver(post_save, sender=InstanceOperation)
@receiver(post_save, sender=GroupOperation)
def tidy_operation(sender,instance,created,**kwargs):
    if not created or instance.serial: return #only following serial operations will not be tidied
    if instance.operation==INSTANCE_OPERATION.remedy.value and instance.script and not instance.tidied:
        supervisor_ops=[s.value for s in INSTANCE_OPERATION]
        ops=utils.remedy_script_tidy(instance.script,supervisor_ops)
        for i in range(len(ops)):
            op=ops[i]
            if i==0:
                if op in supervisor_ops:
                    instance.script=None
                    instance.operation=op
                    instance.tidied=True
                else:
                    instance.script=op
                    instance.tidied=True
                instance.save()
            else:
                if op in supervisor_ops:
                    op_instance=sender(
                        target=instance.target,
                        operation=op,
                    )
                else:
                    op_instance=sender(
                        target=instance.target,
                        operation=INSTANCE_OPERATION.remedy.value,
                        script=op,
                    )
                op_instance.status=OPERATION_STATUS.waiting.value
                op_instance.serial=instance
                op_instance.tidied=True
                op_instance.manual=False
                op_instance.save()
    tidied.send(sender=sender, instance=instance, name='tidied')

@receiver(monitored, sender=Instance)
@receiver(monitored, sender=Group)
@transaction.atomic#to aviod deleted target and duplicated remedy
def select_operation(sender,instance,**kwargs):
    for target in instance.__class__.objects.select_for_update().filter(pk=instance.pk):
        target.remedy(manual=False)
        ops=target.get_next_operations().select_for_update()
        if not ops.exists(): return
        for op in ops:
            if op.runnable:
                op.status=OPERATION_STATUS.running.value
                op.started_time=now()
                op.completed_time=None
                op.save()
                selected.send(sender=op.__class__, instance=op, name='selected')
                break
            else:
                op.status=OPERATION_STATUS.waiting.value
                op.save()

@receiver(selected, sender=InstanceOperation)
@receiver(selected, sender=GroupOperation)
def execute_operation(sender,instance,**kwargs):
    instance.execute()

@receiver(executed, sender=InstanceOperation)
@transaction.atomic
def close_group_operation(sender, instance, **kwargs):
    for running_op in GroupOperation.objects.select_for_update().filter(
        batch_uuid=instance.batch_uuid,
        started_time__isnull=False
    ):
        if not running_op.get_remain_oprations().exists():
            running_op.completed_time=now()
            running_op.status=running_op.get_status()
            running_op.save()
            executed.send(sender=GroupOperation, instance=running_op, name='executed')

@receiver(post_delete, sender=InstanceOperation)
def purge_group_operation(sender, instance, **kwargs):
    g_op=GroupOperation.objects.filter(batch_uuid=instance.batch_uuid).first()
    if g_op and not g_op.get_sub_operations().exists():
        g_op.delete()

@receiver(monitored, sender=Instance)
@receiver(destroyed, sender=Mount)
def cleanup(sender,instance,**kwargs):
    if sender==Instance:
        if not instance.deleting: return
        ms=instance.mount_set.all()
        if ms.exists():
            if instance.umountable:
                for m in ms:
                    m.delete()
                instance.remark+=';umounting'
                instance.save()
        else:
            instance.refresh_from_db()
            if not instance.remark.endswith('umounting'):
                instance.delete()
    else:
        if instance.instance.deleting and not instance.instance.mount_set.all().exists():
            instance.instance.delete()
        if instance.volume.deleting:
            instance.volume.delete()