# QEMU Manual
# https://qemu.weilnetz.de/doc/qemu-doc.html

# For QEMU Monitor Protocol Commands Information, See
# https://qemu.weilnetz.de/doc/qemu-doc.html#pcsys_005fmonitor

import os
import subprocess as sp
import ipaddress

from string import Template
from os.path import join as join_path

from uncloud.common.request import RequestEntry, RequestType
from uncloud.common.vm import VMStatus, declare_stopped
from uncloud.common.network import create_dev, delete_network_interface
from uncloud.common.schemas import VMSchema, NetworkSchema
from uncloud.host import logger
from uncloud.common.shared import shared
from uncloud.vmm import VMM

from marshmallow import ValidationError


class VM:
    def __init__(self, vm_entry):
        self.schema = VMSchema()
        self.vmm = VMM()
        self.key = vm_entry.key
        try:
            self.vm = self.schema.loads(vm_entry.value)
        except ValidationError:
            logger.exception(
                "Couldn't validate VM Entry", vm_entry.value
            )
            self.vm = None
        else:
            self.uuid = vm_entry.key.split("/")[-1]
            self.host_key = self.vm["hostname"]
        logger.debug('VM Hostname {}'.format(self.host_key))

    def get_qemu_args(self):
        command = (
            "-drive file={file},format=raw,if=virtio"
            " -device virtio-rng-pci"
            " -m {memory} -smp cores={cores},threads={threads}"
            " -name {owner}_{name}"
        ).format(
            owner=self.vm["owner"],
            name=self.vm["name"],
            memory=int(self.vm["specs"]["ram"].to_MB()),
            cores=self.vm["specs"]["cpu"],
            threads=1,
            file=shared.storage_handler.qemu_path_string(self.uuid),
        )

        return command.split(" ")

    def start(self, destination_host_key=None):
        migration = False
        if destination_host_key:
            migration = True

        self.create()
        try:
            network_args = self.create_network_dev()
        except Exception as err:
            declare_stopped(self.vm)
            self.vm["log"].append("Cannot Setup Network Properly")
            logger.error("Cannot Setup Network Properly for vm %s", self.uuid, exc_info=err)
        else:
            self.vmm.start(
                uuid=self.uuid,
                migration=migration,
                *self.get_qemu_args(),
                *network_args
            )

            status = self.vmm.get_status(self.uuid)
            logger.debug('VM {} status is {}'.format(self.uuid, status))
            if status == "running":
                self.vm["status"] = VMStatus.running
                self.vm["vnc_socket"] = self.vmm.get_vnc(self.uuid)
            elif status == "inmigrate":
                r = RequestEntry.from_scratch(
                    type=RequestType.TransferVM,  # Transfer VM
                    hostname=self.host_key,  # Which VM should get this request. It is source host
                    uuid=self.uuid,  # uuid of VM
                    destination_sock_path=join_path(
                        self.vmm.socket_dir, self.uuid
                    ),
                    destination_host_key=destination_host_key,  # Where source host transfer VM
                    request_prefix=shared.settings["etcd"]["request_prefix"],
                )
                shared.request_pool.put(r)
            else:
                self.stop()
                declare_stopped(self.vm)
        logger.debug('VM {} has hostname {}'.format(self.uuid, self.vm['hostname']))
        self.sync()

    def stop(self):
        self.vmm.stop(self.uuid)
        self.delete_network_dev()
        declare_stopped(self.vm)
        self.sync()

    def migrate(self, destination_host, destination_sock_path):
        self.vmm.transfer(
            src_uuid=self.uuid,
            destination_sock_path=destination_sock_path,
            host=destination_host,
        )

    def create_network_dev(self):
        command = ""
        for network_mac_and_tap in self.vm["network"]:
            network_name, mac, tap = network_mac_and_tap

            _key = os.path.join(
                shared.settings["etcd"]["network_prefix"],
                self.vm["owner"],
                network_name,
            )
            network = shared.etcd_client.get(_key, value_in_json=True)
            network_schema = NetworkSchema()
            try:
                network = network_schema.load(network.value)
            except ValidationError:
                continue

            if network["type"] == "vxlan":
                tap = create_vxlan_br_tap(
                    _id=network["id"],
                    _dev=shared.settings["network"]["vxlan_phy_dev"],
                    tap_id=tap,
                    ip=network["ipv6"],
                )

                all_networks = shared.etcd_client.get_prefix(
                    shared.settings["etcd"]["network_prefix"],
                    value_in_json=True,
                )

                if ipaddress.ip_network(network["ipv6"]).is_global:
                    update_radvd_conf(all_networks)

            command += (
                "-netdev tap,id=vmnet{net_id},ifname={tap},script=no,downscript=no"
                " -device virtio-net-pci,netdev=vmnet{net_id},mac={mac}".format(
                    tap=tap, net_id=network["id"], mac=mac
                )
            )

        if command:
            command = command.split(' ')

        return command

    def delete_network_dev(self):
        try:
            for network in self.vm["network"]:
                network_name = network[0]
                _ = network[1]  # tap_mac
                tap_id = network[2]

                delete_network_interface("tap{}".format(tap_id))

                owners_vms = shared.vm_pool.by_owner(self.vm["owner"])
                owners_running_vms = shared.vm_pool.by_status(
                    VMStatus.running, _vms=owners_vms
                )

                networks = map(
                    lambda n: n[0],
                    map(lambda vm: vm.network, owners_running_vms),
                )
                networks_in_use_by_user_vms = [vm[0] for vm in networks]
                if network_name not in networks_in_use_by_user_vms:
                    network_entry = resolve_network(
                        network[0], self.vm["owner"]
                    )
                    if network_entry:
                        network_type = network_entry.value["type"]
                        network_id = network_entry.value["id"]
                        if network_type == "vxlan":
                            delete_network_interface(
                                "br{}".format(network_id)
                            )
                            delete_network_interface(
                                "vxlan{}".format(network_id)
                            )
        except Exception:
            logger.exception("Exception in network interface deletion")

    def create(self):
        if shared.storage_handler.is_vm_image_exists(self.uuid):
            # File Already exists. No Problem Continue
            logger.debug("Image for vm %s exists", self.uuid)
        else:
            if shared.storage_handler.make_vm_image(
                src=self.vm["image_uuid"], dest=self.uuid
            ):
                if not shared.storage_handler.resize_vm_image(
                    path=self.uuid,
                    size=int(self.vm["specs"]["os-ssd"].to_MB()),
                ):
                    self.vm["status"] = VMStatus.error
                else:
                    logger.info("New VM Created")

    def sync(self):
        shared.etcd_client.put(
            self.key, self.schema.dump(self.vm), value_in_json=True
        )

    def delete(self):
        self.stop()

        if shared.storage_handler.is_vm_image_exists(self.uuid):
            r_status = shared.storage_handler.delete_vm_image(self.uuid)
            if r_status:
                shared.etcd_client.client.delete(self.key)
        else:
            shared.etcd_client.client.delete(self.key)


def resolve_network(network_name, network_owner):
    network = shared.etcd_client.get(
        join_path(
            shared.settings["etcd"]["network_prefix"],
            network_owner,
            network_name,
        ),
        value_in_json=True,
    )
    return network


def create_vxlan_br_tap(_id, _dev, tap_id, ip=None):
    network_script_base = os.path.join(
        os.path.dirname(os.path.dirname(__file__)), "network"
    )
    vxlan = create_dev(
        script=os.path.join(network_script_base, "create-vxlan.sh"),
        _id=_id,
        dev=_dev,
    )
    if vxlan:
        bridge = create_dev(
            script=os.path.join(
                network_script_base, "create-bridge.sh"
            ),
            _id=_id,
            dev=vxlan,
            ip=ip,
        )
        if bridge:
            tap = create_dev(
                script=os.path.join(
                    network_script_base, "create-tap.sh"
                ),
                _id=str(tap_id),
                dev=bridge,
            )
            if tap:
                return tap


def update_radvd_conf(all_networks):
    network_script_base = os.path.join(
        os.path.dirname(os.path.dirname(__file__)), "network"
    )

    networks = {
        net.value["ipv6"]: net.value["id"]
        for net in all_networks
        if net.value.get("ipv6")
        and ipaddress.ip_network(net.value.get("ipv6")).is_global
    }
    radvd_template = open(
        os.path.join(network_script_base, "radvd-template.conf"), "r"
    ).read()
    radvd_template = Template(radvd_template)

    content = [
        radvd_template.safe_substitute(
            bridge="br{}".format(networks[net]), prefix=net
        )
        for net in networks
        if networks.get(net)
    ]
    with open("/etc/radvd.conf", "w") as radvd_conf:
        radvd_conf.writelines(content)
    try:
        sp.check_output(["systemctl", "restart", "radvd"])
    except sp.CalledProcessError:
        try:
            sp.check_output(["service", "radvd", "restart"])
        except sp.CalledProcessError as err:
            raise err.__class__(
                "Cannot start/restart radvd service", err.cmd
            ) from err
