#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program 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.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys
import copy
import asyncio
import ipaddress

from ...utils.asyncio import locking
from .vmware_gns3_vm import VMwareGNS3VM
from .virtualbox_gns3_vm import VirtualBoxGNS3VM
from .hyperv_gns3_vm import HyperVGNS3VM
from .remote_gns3_vm import RemoteGNS3VM
from .gns3_vm_error import GNS3VMError
from ...version import __version__
from ..compute import ComputeError
from ..controller_error import ControllerError

import logging

log = logging.getLogger(__name__)


class GNS3VM:
    """
    Proxy between the controller and the GNS3 VM engine
    """

    def __init__(self, controller):
        self._controller = controller
        # Keep instance of the loaded engines
        self._engines = {}
        self._settings = {
            "vmname": None,
            "when_exit": "stop",
            "headless": False,
            "enable": False,
            "engine": "vmware",
            "allocate_vcpus_ram": True,
            "ram": 2048,
            "vcpus": 1,
            "port": 80,
        }

    def engine_list(self):
        """
        :returns: Return list of engines supported by GNS3 for the GNS3VM
        """

        download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(
            version=__version__
        )
        vmware_info = {
            "engine_id": "vmware",
            "description": f'VMware is the recommended choice for best performances.<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>.',
            "support_when_exit": True,
            "support_headless": True,
            "support_ram": True,
        }
        if sys.platform.startswith("darwin"):
            vmware_info["name"] = "VMware Fusion (recommended)"
        else:
            vmware_info["name"] = "VMware Workstation / Player (recommended)"

        download_url = (
            "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.Hyper-V.{version}.zip".format(
                version=__version__
            )
        )
        hyperv_info = {
            "engine_id": "hyper-v",
            "name": "Hyper-V",
            "description": f'Hyper-V support (Windows 10/Server 2016 and above). Nested virtualization must be supported and enabled (Intel processor only)<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>',
            "support_when_exit": True,
            "support_headless": False,
            "support_ram": True,
        }

        download_url = (
            "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(
                version=__version__
            )
        )
        virtualbox_info = {
            "engine_id": "virtualbox",
            "name": "VirtualBox",
            "description": f'VirtualBox support. Nested virtualization for both Intel and AMD processors is supported since version 6.1<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>',
            "support_when_exit": True,
            "support_headless": True,
            "support_ram": True,
        }

        remote_info = {
            "engine_id": "remote",
            "name": "Remote",
            "description": "Use a remote GNS3 server as the GNS3 VM.",
            "support_when_exit": False,
            "support_headless": False,
            "support_ram": False,
        }

        engines = [vmware_info, virtualbox_info, remote_info]

        if sys.platform.startswith("win"):
            engines.append(hyperv_info)

        return engines

    def current_engine(self):

        return self._get_engine(self._settings["engine"])

    @property
    def engine(self):

        return self._settings["engine"]

    @property
    def ip_address(self):
        """
        Returns the GNS3 VM IP address.

        :returns: VM IP address
        """

        return self.current_engine().ip_address

    @property
    def running(self):
        """
        Returns if the GNS3 VM is running.

        :returns: Boolean
        """

        return self.current_engine().running

    @property
    def user(self):
        """
        Returns the GNS3 VM user.

        :returns: VM user
        """

        return self.current_engine().user

    @property
    def password(self):
        """
        Returns the GNS3 VM password.

        :returns: VM password
        """

        return self.current_engine().password

    @property
    def port(self):
        """
        Returns the GNS3 VM port.

        :returns: VM port
        """

        return self.current_engine().port

    @property
    def protocol(self):
        """
        Returns the GNS3 VM protocol.

        :returns: VM protocol
        """

        return self.current_engine().protocol

    @property
    def enable(self):
        """
        The GNSVM is activated
        """

        return self._settings.get("enable", False)

    @property
    def when_exit(self):
        """
        What should be done when exit
        """

        return self._settings["when_exit"]

    @property
    def settings(self):

        return self._settings

    @settings.setter
    def settings(self, val):

        self._settings.update(val)

    async def update_settings(self, settings):
        """
        Update settings and will restart the VM if require
        """

        new_settings = copy.copy(self._settings)
        new_settings.update(settings)
        if self.settings != new_settings:
            try:
                await self._stop()
            finally:
                self._settings = settings
                self._controller.save()
            if self.enable:
                await self.start()
        else:
            # When user fix something on his system and try again
            if self.enable and not self.current_engine().running:
                await self.start()

    def _get_engine(self, engine):
        """
        Load an engine
        """

        if engine in self._engines:
            return self._engines[engine]

        if engine == "vmware":
            self._engines["vmware"] = VMwareGNS3VM(self._controller)
            return self._engines["vmware"]
        elif engine == "hyper-v":
            self._engines["hyper-v"] = HyperVGNS3VM(self._controller)
            return self._engines["hyper-v"]
        elif engine == "virtualbox":
            self._engines["virtualbox"] = VirtualBoxGNS3VM(self._controller)
            return self._engines["virtualbox"]
        elif engine == "remote":
            self._engines["remote"] = RemoteGNS3VM(self._controller)
            return self._engines["remote"]
        raise NotImplementedError(f"The engine {engine} for the GNS3 VM is not supported")

    def asdict(self):
        return self._settings

    @locking
    async def list(self, engine):
        """
        List VMS for an engine
        """

        engine = self._get_engine(engine)
        vms = []
        try:
            for vm in await engine.list():
                vms.append({"vmname": vm["vmname"]})
        except GNS3VMError as e:
            # We raise error only if user activated the GNS3 VM
            # otherwise you have noise when VMware is not installed
            if self.enable:
                raise e
        return vms

    async def auto_start_vm(self):
        """
        Auto start the GNS3 VM if require
        """

        if self.enable:
            try:
                await self.start()
            except GNS3VMError as e:
                # User will receive the error later when they will try to use the node
                try:
                    compute = await self._controller.add_compute(
                        compute_id="vm", name=f"GNS3 VM ({self.current_engine().vmname})", host=None, force=True
                    )
                    compute.set_last_error(str(e))

                except ControllerError:
                    pass
                log.error(f"Cannot start the GNS3 VM: {e}")

    async def exit_vm(self):

        if self.enable:
            try:
                if self._settings["when_exit"] == "stop":
                    await self._stop()
                elif self._settings["when_exit"] == "suspend":
                    await self._suspend()
            except GNS3VMError as e:
                log.warning(str(e))

    @locking
    async def start(self):
        """
        Start the GNS3 VM
        """

        engine = self.current_engine()
        if not engine.running:
            if self._settings["vmname"] is None:
                return
            log.info("Start the GNS3 VM")
            engine.allocate_vcpus_ram = self._settings["allocate_vcpus_ram"]
            engine.vmname = self._settings["vmname"]
            engine.ram = self._settings["ram"]
            engine.vcpus = self._settings["vcpus"]
            engine.headless = self._settings["headless"]
            engine.port = self._settings["port"]
            compute = await self._controller.add_compute(
                compute_id="vm", name=f"GNS3 VM is starting ({engine.vmname})", host=None, force=True, connect=False
            )

            try:
                await engine.start()
            except Exception as e:
                await self._controller.delete_compute("vm")
                log.error(f"Cannot start the GNS3 VM: {str(e)}")
                await compute.update(name=f"GNS3 VM ({engine.vmname})")
                compute.set_last_error(str(e))
                raise e
            await compute.connect()  # we can connect now that the VM has started
            await compute.update(
                name=f"GNS3 VM ({engine.vmname})",
                protocol=self.protocol,
                host=self.ip_address,
                port=self.port,
                user=self.user,
                password=self.password,
            )

            # check if the VM is in the same subnet as the local server, start 10 seconds later to give
            # some time for the compute in the VM to be ready for requests
            asyncio.get_event_loop().call_later(10, lambda: asyncio.ensure_future(self._check_network(compute)))

    async def _check_network(self, compute):
        """
        Check that the VM is in the same subnet as the local server
        """

        try:
            vm_interfaces = await compute.interfaces()
            vm_interface_netmask = None
            for interface in vm_interfaces:
                if interface["ip_address"] == self.ip_address:
                    vm_interface_netmask = interface["netmask"]
                    break
            if vm_interface_netmask:
                vm_network = ipaddress.ip_interface(f"{compute.host_ip}/{vm_interface_netmask}").network
                for compute_id in self._controller.computes:
                    if compute_id == "local":
                        compute = self._controller.get_compute(compute_id)
                        interfaces = await compute.interfaces()
                        netmask = None
                        for interface in interfaces:
                            if interface["ip_address"] == compute.host_ip:
                                netmask = interface["netmask"]
                                break
                        if netmask:
                            compute_network = ipaddress.ip_interface(f"{compute.host_ip}/{netmask}").network
                            if vm_network.compare_networks(compute_network) != 0:
                                msg = "The GNS3 VM (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format(
                                    self.ip_address, vm_network, compute_id, compute.host_ip, compute_network
                                )
                                self._controller.notification.controller_emit("log.warning", {"message": msg})
        except ComputeError as e:
            log.warning(f"Could not check the VM is in the same subnet as the local server: {e}")
        except ControllerError as e:
            log.warning(f"Could not check the VM is in the same subnet as the local server: {e}")

    @locking
    async def _suspend(self):
        """
        Suspend the GNS3 VM
        """
        engine = self.current_engine()
        if "vm" in self._controller.computes:
            await self._controller.delete_compute("vm")
        if engine.running:
            log.info("Suspend the GNS3 VM")
            await engine.suspend()

    @locking
    async def _stop(self):
        """
        Stop the GNS3 VM
        """
        engine = self.current_engine()
        if "vm" in self._controller.computes:
            await self._controller.delete_compute("vm")
        if engine.running:
            log.info("Stop the GNS3 VM")
            await engine.stop()