"""
Utilities to help make requests to virtualbox

The virtualbox SDK reference can be found at http://download.virtualbox.org/virtualbox/SDKRef.pdf

This code assumes vboxapi.py from VirtualBox distribution
being in PYTHONPATH, or installed system-wide
"""

import logging
import re
import time

import salt.utils.compat
import salt.utils.data
from salt.utils.timeout import wait_for

log = logging.getLogger(__name__)


HAS_LIBS = False
try:
    import vboxapi

    HAS_LIBS = True

except ImportError:
    VirtualBoxManager = None
    log.trace("Couldn't import VirtualBox API")

_virtualboxManager = None

"""
Attributes we expect to have when converting an XPCOM object to a dict
"""
XPCOM_ATTRIBUTES = {
    "IMachine": [
        "id",
        "name",
        "accessible",
        "description",
        "groups",
        "memorySize",
        "OSTypeId",
        "state",
    ],
    "INetworkAdapter": [
        "adapterType",
        "slot",
        "enabled",
        "MACAddress",
        "bridgedInterface",
        "hostOnlyInterface",
        "internalNetwork",
        "NATNetwork",
        "genericDriver",
        "cableConnected",
        "lineSpeed",
        "lineSpeed",
    ],
}

UNKNOWN_MACHINE_STATE = ("Unknown", "This state is unknown to us. Might be new?")
MACHINE_STATE_LIST = [
    ("Null", "Null value (never used by the API)"),
    (
        "PoweredOff",
        "The machine is not running and has no saved execution state; "
        "it has either never been started or been shut down successfully.",
    ),
    (
        "Saved",
        "The machine is not currently running, but the execution state of the machine"
        " has been saved to an external file when it was running, from where it can be"
        " resumed.",
    ),
    (
        "Teleported",
        "The machine was teleported to a different host (or process) and then powered"
        " off. Take care when powering it on again may corrupt resources it shares with"
        " the teleportation target (e.g. disk and network).",
    ),
    (
        "Aborted",
        "The process running the machine has terminated abnormally. This may indicate a"
        " crash of the VM process in host execution context, or the VM process has been"
        " terminated externally.",
    ),
    ("Running", "The machine is currently being executed."),
    ("Paused", "Execution of the machine has been paused."),
    (
        "Stuck",
        "Execution of the machine has reached the 'Guru Meditation' condition. This"
        " indicates a severe error in the hypervisor itself.",
    ),
    (
        "Teleporting",
        "The machine is about to be teleported to a different host or process. It is"
        " possible to pause a machine in this state, but it will go to the"
        " TeleportingPausedVM state and it will not be possible to resume it again"
        " unless the teleportation fails.",
    ),
    (
        "LiveSnapshotting",
        "A live snapshot is being taken. The machine is running normally, but some of"
        " the runtime configuration options are inaccessible. Also, if paused while in"
        " this state it will transition to OnlineSnapshotting and it will not be resume"
        " the execution until the snapshot operation has completed.",
    ),
    (
        "Starting",
        "Machine is being started after powering it on from a zero execution state.",
    ),
    (
        "Stopping",
        "Machine is being normally stopped powering it off, or after the guest OS has"
        " initiated a shutdown sequence.",
    ),
    ("Saving", "Machine is saving its execution state to a file."),
    (
        "Restoring",
        "Execution state of the machine is being restored from a file after powering it"
        " on from the saved execution state.",
    ),
    (
        "TeleportingPausedVM",
        "The machine is being teleported to another host or process, but it is not "
        "running. This is the paused variant of the Teleporting state.",
    ),
    ("TeleportingIn", "Teleporting the machine state in from another host or process."),
    (
        "FaultTolerantSyncing",
        "The machine is being synced with a fault tolerant VM running else-where.",
    ),
    (
        "DeletingSnapshotOnline",
        "Like DeletingSnapshot , but the merging of media is ongoing in the "
        "background while the machine is running.",
    ),
    (
        "DeletingSnapshotPaused",
        "Like DeletingSnapshotOnline , but the machine was paused when "
        "the merging of differencing media was started.",
    ),
    (
        "OnlineSnapshotting",
        "Like LiveSnapshotting , but the machine was paused when the merging "
        "of differencing media was started.",
    ),
    (
        "RestoringSnapshot",
        "A machine snapshot is being restored; this typically does not take long.",
    ),
    (
        "DeletingSnapshot",
        "A machine snapshot is being deleted; this can take a long time since this may"
        " require merging differencing media. This value indicates that the machine is"
        " not running while the snapshot is being deleted.",
    ),
    ("SettingUp", "Lengthy setup operation is in progress."),
    ("Snapshotting", "Taking an (offline) snapshot."),
    (
        "FirstOnline",
        "Pseudo-state: first online state (for use in relational expressions).",
    ),
    (
        "LastOnline",
        "Pseudo-state: last online state (for use in relational expressions).",
    ),
    (
        "FirstTransient",
        "Pseudo-state: first transient state (for use in relational expressions).",
    ),
    (
        "LastTransient",
        "Pseudo-state: last transient state (for use in relational expressions).",
    ),
]
MACHINE_STATES = dict(MACHINE_STATE_LIST)

"""
Dict of states {
    <number>: ( <name>, <description> )
}
"""
MACHINE_STATES_ENUM = dict(enumerate(MACHINE_STATE_LIST))


def vb_get_manager():
    """
    Creates a 'singleton' manager to communicate with a local virtualbox hypervisor.
    @return:
    @rtype: VirtualBoxManager
    """
    global _virtualboxManager
    if _virtualboxManager is None and HAS_LIBS:
        salt.utils.compat.reload(vboxapi)
        _virtualboxManager = vboxapi.VirtualBoxManager(None, None)

    return _virtualboxManager


def vb_get_box():
    """
    Needed for certain operations in the SDK e.g creating sessions
    @return:
    @rtype: IVirtualBox
    """
    vb_get_manager()

    try:
        # This works in older versions of the SDK, but does not seem to work anymore.
        vbox = _virtualboxManager.vbox
    except AttributeError:
        vbox = _virtualboxManager.getVirtualBox()

    return vbox


def vb_get_max_network_slots():
    """
    Max number of slots any machine can have
    @return:
    @rtype: number
    """
    sysprops = vb_get_box().systemProperties
    totals = [
        sysprops.getMaxNetworkAdapters(adapter_type)
        for adapter_type in [
            1,  # PIIX3 A PIIX3 (PCI IDE ISA Xcelerator) chipset.
            2,  # ICH9 A ICH9 (I/O Controller Hub) chipset
        ]
    ]
    return sum(totals)


def vb_get_network_adapters(machine_name=None, machine=None):
    """
    A valid machine_name or a machine is needed to make this work!

    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @return: INetorkAdapter's converted to dicts
    @rtype: [dict]
    """

    if machine_name:
        machine = vb_get_box().findMachine(machine_name)
    network_adapters = []

    for i in range(vb_get_max_network_slots()):
        try:
            inetwork_adapter = machine.getNetworkAdapter(i)
            network_adapter = vb_xpcom_to_attribute_dict(
                inetwork_adapter, "INetworkAdapter"
            )
            network_adapter["properties"] = inetwork_adapter.getProperties("")
            network_adapters.append(network_adapter)
        except Exception:  # pylint: disable=broad-except
            pass

    return network_adapters


def vb_wait_for_network_address(
    timeout, step=None, machine_name=None, machine=None, wait_for_pattern=None
):
    """
    Wait until a machine has a network address to return or quit after the timeout

    @param timeout: in seconds
    @type timeout: float
    @param step: How regularly we want to check for ips (in seconds)
    @type step: float
    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @type wait_for_pattern: str
    @param wait_for_pattern:
    @type machine: str
    @return:
    @rtype: list
    """
    kwargs = {
        "machine_name": machine_name,
        "machine": machine,
        "wait_for_pattern": wait_for_pattern,
    }
    return wait_for(
        vb_get_network_addresses,
        timeout=timeout,
        step=step,
        default=[],
        func_kwargs=kwargs,
    )


def _check_session_state(xp_session, expected_state="Unlocked"):
    """
    @param xp_session:
    @type xp_session: ISession from the Virtualbox API
    @param expected_state: The constant descriptor according to the docs
    @type expected_state: str
    @return:
    @rtype: bool
    """
    state_value = getattr(
        _virtualboxManager.constants, "SessionState_" + expected_state
    )
    return xp_session.state == state_value


def vb_wait_for_session_state(xp_session, state="Unlocked", timeout=10, step=None):
    """
    Waits until a session state has been reached, checking at regular intervals.

    @param xp_session:
    @type xp_session: ISession from the Virtualbox API
    @param state: The constant descriptor according to the docs
    @type state: str
    @param timeout: in seconds
    @type timeout: int | float
    @param step: Intervals at which the value is checked
    @type step: int | float
    @return: Did we reach the state?
    @rtype: bool
    """
    args = (xp_session, state)
    wait_for(
        _check_session_state, timeout=timeout, step=step, default=False, func_args=args
    )


def vb_get_network_addresses(machine_name=None, machine=None, wait_for_pattern=None):
    """
    TODO distinguish between private and public addresses

    A valid machine_name or a machine is needed to make this work!

    !!!
    Guest prerequisite: GuestAddition
    !!!

    Thanks to Shrikant Havale for the StackOverflow answer http://stackoverflow.com/a/29335390

    More information on guest properties: https://www.virtualbox.org/manual/ch04.html#guestadd-guestprops

    @param machine_name:
    @type machine_name: str
    @param machine:
    @type machine: IMachine
    @return: All the IPv4 addresses we could get
    @rtype: str[]
    """
    if machine_name:
        machine = vb_get_box().findMachine(machine_name)

    ip_addresses = []
    log.debug("checking for power on:")
    if machine.state == _virtualboxManager.constants.MachineState_Running:

        log.debug("got power on:")

        # wait on an arbitrary named property
        # for instance use a dhcp client script to set a property via VBoxControl guestproperty set dhcp_done 1
        if wait_for_pattern and not machine.getGuestPropertyValue(wait_for_pattern):
            log.debug("waiting for pattern:%s:", wait_for_pattern)
            return None

        _total_slots = machine.getGuestPropertyValue("/VirtualBox/GuestInfo/Net/Count")

        # upon dhcp the net count drops to 0 and it takes some seconds for it to be set again
        if not _total_slots:
            log.debug("waiting for net count:%s:", wait_for_pattern)
            return None

        try:
            total_slots = int(_total_slots)
            for i in range(total_slots):
                try:
                    address = machine.getGuestPropertyValue(
                        f"/VirtualBox/GuestInfo/Net/{i}/V4/IP"
                    )
                    if address:
                        ip_addresses.append(address)
                except Exception as e:  # pylint: disable=broad-except
                    log.debug(e.message)
        except ValueError as e:
            log.debug(e.message)
            return None

    log.debug("returning ip_addresses:%s:", ip_addresses)
    return ip_addresses


def vb_list_machines(**kwargs):
    """
    Which machines does the hypervisor have
    @param kwargs: Passed to vb_xpcom_to_attribute_dict to filter the attributes
    @type kwargs: dict
    @return: Untreated dicts of the machines known to the hypervisor
    @rtype: [{}]
    """
    manager = vb_get_manager()
    machines = manager.getArray(vb_get_box(), "machines")
    return [
        vb_xpcom_to_attribute_dict(machine, "IMachine", **kwargs)
        for machine in machines
    ]


def vb_create_machine(name=None):
    """
    Creates a machine on the virtualbox hypervisor

    TODO pass more params to customize machine creation
    @param name:
    @type name: str
    @return: Representation of the created machine
    @rtype: dict
    """
    vbox = vb_get_box()
    log.info("Create virtualbox machine %s ", name)
    groups = None
    os_type_id = "Other"
    new_machine = vbox.createMachine(
        None, name, groups, os_type_id, None  # Settings file  # flags
    )
    vbox.registerMachine(new_machine)
    log.info("Finished creating %s", name)
    return vb_xpcom_to_attribute_dict(new_machine, "IMachine")


def vb_clone_vm(name=None, clone_from=None, clone_mode=0, timeout=10000, **kwargs):
    """
    Tells virtualbox to create a VM by cloning from an existing one

    @param name: Name for the new VM
    @type name: str
    @param clone_from:
    @type clone_from: str
    @param timeout: maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return dict of resulting VM
    """
    vbox = vb_get_box()
    log.info("Clone virtualbox machine %s from %s", name, clone_from)

    source_machine = vbox.findMachine(clone_from)

    groups = None
    os_type_id = "Other"
    new_machine = vbox.createMachine(
        None, name, groups, os_type_id, None  # Settings file  # flags
    )

    progress = source_machine.cloneTo(
        new_machine, clone_mode, None  # CloneMode  # CloneOptions : None = Full?
    )

    progress.waitForCompletion(timeout)
    log.info("Finished cloning %s from %s", name, clone_from)

    vbox.registerMachine(new_machine)

    return vb_xpcom_to_attribute_dict(new_machine, "IMachine")


def _start_machine(machine, session):
    """
    Helper to try and start machines

    @param machine:
    @type machine: IMachine
    @param session:
    @type session: ISession
    @return:
    @rtype: IProgress or None
    """
    try:
        return machine.launchVMProcess(session, "", "")
    except Exception as e:  # pylint: disable=broad-except
        log.debug(e.message, exc_info=True)
        return None


def vb_start_vm(name=None, timeout=10000, **kwargs):
    """
    Tells Virtualbox to start up a VM.
    Blocking function!

    @param name:
    @type name: str
    @param timeout: Maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return untreated dict of started VM
    """
    # Time tracking
    start_time = time.time()
    timeout_in_seconds = timeout / 1000
    max_time = start_time + timeout_in_seconds

    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    session = _virtualboxManager.getSessionObject(vbox)

    log.info(
        "Starting machine %s in state %s", name, vb_machinestate_to_str(machine.state)
    )
    try:
        # Keep trying to start a machine
        args = (machine, session)
        progress = wait_for(_start_machine, timeout=timeout_in_seconds, func_args=args)
        if not progress:
            progress = machine.launchVMProcess(session, "", "")

        # We already waited for stuff, don't push it
        time_left = max_time - time.time()
        progress.waitForCompletion(time_left * 1000)
    finally:
        _virtualboxManager.closeMachineSession(session)

    # The session state should best be unlocked otherwise subsequent calls might cause problems
    time_left = max_time - time.time()
    vb_wait_for_session_state(session, timeout=time_left)
    log.info("Started machine %s", name)

    return vb_xpcom_to_attribute_dict(machine, "IMachine")


def vb_stop_vm(name=None, timeout=10000, **kwargs):
    """
    Tells Virtualbox to stop a VM.
    This is a blocking function!

    @param name:
    @type name: str
    @param timeout: Maximum time in milliseconds to wait or -1 to wait indefinitely
    @type timeout: int
    @return untreated dict of stopped VM
    """
    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    log.info("Stopping machine %s", name)
    session = _virtualboxManager.openMachineSession(machine)
    try:
        console = session.console
        progress = console.powerDown()
        progress.waitForCompletion(timeout)
    finally:
        _virtualboxManager.closeMachineSession(session)
        vb_wait_for_session_state(session)
    log.info(
        "Stopped machine %s is now %s", name, vb_machinestate_to_str(machine.state)
    )
    return vb_xpcom_to_attribute_dict(machine, "IMachine")


def vb_destroy_machine(name=None, timeout=10000):
    """
    Attempts to get rid of a machine and all its files from the hypervisor
    @param name:
    @type name: str
    @param timeout int timeout in milliseconds
    """
    vbox = vb_get_box()
    log.info("Destroying machine %s", name)
    machine = vbox.findMachine(name)
    files = machine.unregister(2)
    progress = machine.deleteConfig(files)
    progress.waitForCompletion(timeout)
    log.info("Finished destroying machine %s", name)


def vb_xpcom_to_attribute_dict(
    xpcom,
    interface_name=None,
    attributes=None,
    excluded_attributes=None,
    extra_attributes=None,
):
    """
    Attempts to build a dict from an XPCOM object.
    Attributes that don't exist in the object return an empty string.

    attribute_list = list of str or tuple(str,<a class>)

    e.g attributes=[('bad_attribute', list)] --> { 'bad_attribute': [] }

    @param xpcom:
    @type xpcom:
    @param interface_name: Which interface we will be converting from.
                           Without this it's best to specify the list of attributes you want
    @type interface_name: str
    @param attributes: Overrides the attributes used from XPCOM_ATTRIBUTES
    @type attributes: attribute_list
    @param excluded_attributes: Which should be excluded in the returned dict.
                                !!These take precedence over extra_attributes!!
    @type excluded_attributes: attribute_list
    @param extra_attributes: Which should be retrieved in addition those already being retrieved
    @type extra_attributes: attribute_list
    @return:
    @rtype: dict
    """
    # Check the interface
    if interface_name:
        m = re.search(rf"XPCOM.+implementing {interface_name}", str(xpcom))
        if not m:
            # TODO maybe raise error here?
            log.warning(
                "Interface %s is unknown and cannot be converted to dict",
                interface_name,
            )
            return dict()

    interface_attributes = set(attributes or XPCOM_ATTRIBUTES.get(interface_name, []))
    if extra_attributes:
        interface_attributes = interface_attributes.union(extra_attributes)
    if excluded_attributes:
        interface_attributes = interface_attributes.difference(excluded_attributes)

    attribute_tuples = []
    for attribute in interface_attributes:
        if isinstance(attribute, tuple):
            attribute_name = attribute[0]
            attribute_class = attribute[1]
            value = (attribute_name, getattr(xpcom, attribute_name, attribute_class()))
        else:
            value = (attribute, getattr(xpcom, attribute, ""))
        attribute_tuples.append(value)

    return dict(attribute_tuples)


def treat_machine_dict(machine):
    """
    Make machine presentable for outside world.

    !!!Modifies the input machine!!!

    @param machine:
    @type machine: dict
    @return: the modified input machine
    @rtype: dict
    """
    machine.update(
        {
            "id": machine.get("id", ""),
            "image": machine.get("image", ""),
            "size": "{} MB".format(machine.get("memorySize", 0)),
            "state": machine_get_machinestate_str(machine),
            "private_ips": [],
            "public_ips": [],
        }
    )

    # Replaced keys
    if "memorySize" in machine:
        del machine["memorySize"]
    return machine


def vb_machinestate_to_str(machinestate):
    """
    Put a name to the state

    @param machinestate: from the machine state enum from XPCOM
    @type machinestate: int
    @return:
    @rtype: str
    """

    return vb_machinestate_to_tuple(machinestate)[0]


def vb_machinestate_to_description(machinestate):
    """
    Describe the given state

    @param machinestate: from the machine state enum from XPCOM
    @type machinestate: int | str
    @return:
    @rtype: str
    """
    return vb_machinestate_to_tuple(machinestate)[1]


def vb_machinestate_to_tuple(machinestate):
    """

    @param machinestate:
    @type machinestate: int | str
    @return:
    @rtype: tuple(<name>, <description>)
    """
    if isinstance(machinestate, int):
        ret = MACHINE_STATES_ENUM.get(machinestate, UNKNOWN_MACHINE_STATE)
    elif isinstance(machinestate, str):
        ret = MACHINE_STATES.get(machinestate, UNKNOWN_MACHINE_STATE)
    else:
        ret = UNKNOWN_MACHINE_STATE
    return salt.utils.data.decode(ret, preserve_tuples=True)


def machine_get_machinestate_tuple(machinedict):
    return vb_machinestate_to_tuple(machinedict.get("state"))


def machine_get_machinestate_str(machinedict):
    return vb_machinestate_to_str(machinedict.get("state"))


def vb_machine_exists(name):
    """
    Checks in with the hypervisor to see if the machine with the given name is known
    @param name:
    @type name:
    @return:
    @rtype:
    """
    try:
        vbox = vb_get_box()
        vbox.findMachine(name)
        return True
    except Exception as e:  # pylint: disable=broad-except
        if isinstance(e.message, str):
            message = e.message
        elif hasattr(e, "msg") and isinstance(getattr(e, "msg"), str):
            message = getattr(e, "msg")
        else:
            message = ""
        if 0 > message.find("Could not find a registered machine named"):
            log.error(message)

        return False


def vb_get_machine(name, **kwargs):
    """
    Attempts to fetch a machine from Virtualbox and convert it to a dict

    @param name: The unique name of the machine
    @type name:
    @param kwargs: To be passed to vb_xpcom_to_attribute_dict
    @type kwargs:
    @return:
    @rtype: dict
    """
    vbox = vb_get_box()
    machine = vbox.findMachine(name)
    return vb_xpcom_to_attribute_dict(machine, "IMachine", **kwargs)
