"""
    Provide functions to find and update VM's.
"""

import logging
import os
import platform
import time

from .config import config
from .pkgmgr import get_pkgmgrs, run_pkgmgr
from .virtualizers import get_virtualizer, VM_STOPPED, VM_UNKNOWN
from .vm import VM

log = logging.getLogger(__name__)


def update_all_vms():
    """
        Update all virtual machines on the system.

        :return: exitcode
        :rtype: int
    """

    log.info('Starting update on all VMs')

    vms = _get_all_vms()

    log.info('Found %i VM(s) to update', len(vms))

    available_ports = iter(_get_available_ports(vms))

    for vm in vms:
        vm_orig_status = VM_UNKNOWN

        try:
            if not any(vm.get_ssh_info()):
                if vm.get_status() == VM_STOPPED:
                    log.info('Enabling SSH for %s', vm.uid)
                    vm.enable_ssh(next(available_ports))
                else:
                    log.warn('SSH cannot be enabled for %s unless it is stopped', vm.uid)
                    log.info('Skipping %s', vm.uid)

                    continue

            log.info('Starting update on %s', vm.uid)

            vm_orig_status = vm.get_status()

            if vm_orig_status == VM_STOPPED:
                log.info('Starting %s', vm.uid)
                vm.start()
                time.sleep(config.general.wait_after_start)

            for pkgmgr, cmds in get_pkgmgrs(vm):
                run_pkgmgr(vm, pkgmgr, cmds)
        except:
            log.exception('Failed while updating %s', vm.uid)

        if vm_orig_status == VM_STOPPED:
            log.info('Stopping %s', vm.uid)
            time.sleep(config.general.wait_before_stop)
            vm.stop()

        log.info('Finished update on %s', vm.uid)

    log.info('Finished update on all VMs')

    return 0


def _get_all_vms():
    """
        Return all virtual machines on the system.

        Ignores machines where ``Ignore`` is ``True`` in the config.

        :return: list of virtual machines
        :rtype: list(:class:`~.vm.VM`)
    """

    vms = []

    virtualizers = _find_virtualizers()

    for virt_name, virt_path in virtualizers.items():
        log.info('Querying virtualizer %s', virt_name)

        virtualizer = get_virtualizer(virt_name, virt_path)

        for vm_name, vm_uuid in virtualizer.list_vms():
            if _should_skip(vm_name):
                log.info('Skipping VM %s', vm_name)
            else:
                log.info('Found VM %s', vm_name)

                vms.append(VM(virtualizer, vm_name))

    return vms


def _find_virtualizers():
    """
        Return all virtualizers found on the system.

        :return: dictionary of names and paths
        :rtype: dict(str, str)
    """

    log.info('Finding virtualizers')

    virtualizers = {}

    for name, paths in config.virtualizers[platform.system()].items():
        try:
            for path in paths:
                path = os.path.expandvars(path)

                log.debug('Checking virtualizer "%s"', path)

                if os.path.isfile(path):
                    log.debug('Found virtualizer "%s"', path)

                    virtualizers[name] = path
        except:
            log.exception('Failed while locating virtualizer %s', name)

    return virtualizers


def _get_available_ports(vms):
    """
        Return ports in the configured range not in use by the ``vms``.

        :param vms: list of virtual machines
        :type vms: list(:class:`~.vm.VM`)

        :return: list of ports
        :rtype: list(int)
    """

    used_ports = _get_used_ports(vms)

    min_port = config.network.ssh.host_min_port
    max_port = config.network.ssh.host_max_port

    return [p for p in range(min_port, max_port) if p not in used_ports]


def _get_used_ports(vms):
    """
        Return a set of ports in use by the ``vms``.

        :param vms: list of virtual machines
        :type vms: list(:class:`~.vm.VM`)

        :return: set of ports
        :rtype: set(int)
    """

    used_ports = set()

    for vm in vms:
        ip, port = vm.get_ssh_info()

        if port:
            used_ports.add(port)

    return used_ports


def _should_skip(name):
    """
        Return whether to skip a machine.

        :param str name: name of the machine to check

        :rtype: bool
    """

    return name in config.machines and config.machines[name].ignore
