# Copyright (C) 2011, 2013 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

import queue
import threading

from virtinst import log

from ..baseclass import vmmGObject
from ..connmanager import vmmConnectionManager
from ..object.domain import vmmInspectionApplication, vmmInspectionData


def _inspection_error(_errstr):
    data = vmmInspectionData()
    data.errorstr = _errstr
    return data


def _make_fake_data(vm):
    """
    Return fake vmmInspectionData for use with the test driver
    """
    if not vm.xmlobj.devices.disk:
        return _inspection_error("Fake test error no disks")

    data = vmmInspectionData()
    data.os_type = "test_os_type"
    data.distro = "test_distro"
    data.major_version = 123
    data.minor_version = 456
    data.hostname = "test_hostname"
    data.product_name = "test_product_name"
    data.product_variant = "test_product_variant"

    from gi.repository import Gtk

    icontheme = Gtk.IconTheme.get_default()
    icon = icontheme.lookup_icon("vm_new", Gtk.IconSize.LARGE_TOOLBAR, 0)
    data.icon = open(icon.get_filename(), "rb").read()

    data.applications = []
    for prefix in ["test_app1_", "test_app2_"]:
        import time

        app = vmmInspectionApplication()
        if "app1" in prefix:
            app.display_name = prefix + "display_name"
            app.summary = prefix + "summary-" + str(time.time())
        else:
            app.name = prefix + "name"
            app.description = prefix + "description-" + str(time.time()) + "\n"
        app.epoch = 1
        app.version = "2"
        app.release = "3"
        data.applications.append(app)

    return data


def _perform_inspection(conn, vm):  # pragma: no cover
    """
    Perform the actual guestfs interaction and return results in
    a vmmInspectionData object
    """
    import guestfs  # pylint: disable=import-error

    g = guestfs.GuestFS(close_on_exit=False, python_return_dict=True)
    prettyvm = conn.get_uri() + ":" + vm.get_name()
    try:
        g.add_libvirt_dom(vm.get_backend(), readonly=1)
        g.launch()
    except Exception as e:
        log.debug("%s: Error launching libguestfs appliance: %s", prettyvm, str(e))
        return _inspection_error(_("Error launching libguestfs appliance: %s") % str(e))

    log.debug("%s: inspection appliance connected", prettyvm)

    # Inspect the operating system.
    roots = g.inspect_os()
    if len(roots) == 0:
        log.debug("%s: no operating systems found", prettyvm)
        return _inspection_error(_("Inspection found no operating systems."))

    # Arbitrarily pick the first root device.
    root = roots[0]

    # Inspection results.
    os_type = g.inspect_get_type(root)  # eg. "linux"
    distro = g.inspect_get_distro(root)  # eg. "fedora"
    major_version = g.inspect_get_major_version(root)  # eg. 14
    minor_version = g.inspect_get_minor_version(root)  # eg. 0
    hostname = g.inspect_get_hostname(root)  # string
    product_name = g.inspect_get_product_name(root)  # string
    product_variant = g.inspect_get_product_variant(root)  # string
    package_format = g.inspect_get_package_format(root)  # string

    # For inspect_list_applications and inspect_get_icon we
    # require that the guest filesystems are mounted.  However
    # don't fail if this is not possible (I'm looking at you,
    # FreeBSD).
    filesystems_mounted = False
    # Mount up the disks, like guestfish --ro -i.

    # Sort keys by length, shortest first, so that we end up
    # mounting the filesystems in the correct order.
    mps = g.inspect_get_mountpoints(root)

    mps = sorted(mps.items(), key=lambda k: len(k[0]))
    for mp, dev in mps:
        try:
            g.mount_ro(dev, mp)
            filesystems_mounted = True
        except Exception:
            log.exception("%s: exception mounting %s on %s (ignored)", prettyvm, dev, mp)

    icon = None
    apps = None
    if filesystems_mounted:
        # string containing PNG data
        icon = g.inspect_get_icon(root, favicon=0, highquality=1)
        if icon is None or len(icon) == 0:
            # no high quality icon, try a low quality one
            icon = g.inspect_get_icon(root, favicon=0, highquality=0)
            if icon is None or len(icon) == 0:
                icon = None

        # Inspection applications.
        try:
            gapps = g.inspect_list_applications2(root)
            # applications listing worked, so make apps a real list
            # (instead of None)
            apps = []
            for gapp in gapps:
                app = vmmInspectionApplication()
                if gapp["app2_name"]:
                    app.name = gapp["app2_name"]
                if gapp["app2_display_name"]:
                    app.display_name = gapp["app2_display_name"]
                app.epoch = gapp["app2_epoch"]
                if gapp["app2_version"]:
                    app.version = gapp["app2_version"]
                if gapp["app2_release"]:
                    app.release = gapp["app2_release"]
                if gapp["app2_summary"]:
                    app.summary = gapp["app2_summary"]
                if gapp["app2_description"]:
                    app.description = gapp["app2_description"]
                apps.append(app)
        except Exception:
            log.exception("%s: exception while listing apps (ignored)", prettyvm)

    # Force the libguestfs handle to close right now.
    del g

    # Log what we found.
    log.debug(
        "%s: detected operating system: %s %s %d.%d (%s) (%s)",
        prettyvm,
        os_type,
        distro,
        major_version,
        minor_version,
        product_name,
        package_format,
    )
    log.debug("hostname: %s", hostname)
    if icon:
        log.debug("icon: %d bytes", len(icon))
    if apps:
        log.debug("# apps: %d", len(apps))

    data = vmmInspectionData()
    data.os_type = str(os_type)
    data.distro = str(distro)
    data.major_version = int(major_version)
    data.minor_version = int(minor_version)
    data.hostname = str(hostname)
    data.product_name = str(product_name)
    data.product_variant = str(product_variant)
    data.icon = icon
    data.applications = list(apps or [])
    data.package_format = str(package_format)

    return data


class vmmInspection(vmmGObject):
    _libguestfs_installed = None

    @classmethod
    def get_instance(cls):
        if not cls._instance:
            if not cls.libguestfs_installed():
                return None  # pragma: no cover
            cls._instance = vmmInspection()
        return cls._instance

    @classmethod
    def libguestfs_installed(cls):
        if cls._libguestfs_installed is None:
            try:
                import guestfs as ignore  # pylint: disable=import-error

                log.debug("python guestfs is installed")
                cls._libguestfs_installed = True
            except ImportError:  # pragma: no cover
                log.debug("python guestfs is not installed")
                cls._libguestfs_installed = False
            except Exception:  # pragma: no cover
                log.debug("error importing guestfs", exc_info=True)
                cls._libguestfs_installed = False
        return cls._libguestfs_installed

    def __init__(self):
        vmmGObject.__init__(self)
        self._cleanup_on_app_close()

        self._thread = None

        self._q = queue.Queue()
        self._cached_data = {}
        self._uris = []

        val = self.config.get_libguestfs_inspect_vms()
        log.debug("libguestfs gsetting enabled=%s", str(val))
        if not val:
            return

        connmanager = vmmConnectionManager.get_instance()
        connmanager.connect("conn-added", self._conn_added_cb)
        connmanager.connect("conn-removed", self._conn_removed_cb)
        for conn in connmanager.conns.values():
            self._conn_added_cb(connmanager, conn)  # pragma: no cover

        self._start()

    def _cleanup(self):
        self._stop()
        self._q = queue.Queue()
        self._cached_data = {}

    def _conn_added_cb(self, connmanager, conn):
        uri = conn.get_uri()
        if uri in self._uris:
            return  # pragma: no cover

        self._uris.append(uri)
        conn.connect("vm-added", self._vm_added_cb)
        for vm in conn.list_vms():  # pragma: no cover
            self._vm_added_cb(conn, vm.get_name())

    def _conn_removed_cb(self, connmanager, uri):
        self._uris.remove(uri)

    def _vm_added_cb(self, conn, vm):
        # Called by the main thread whenever a VM is added to vmlist.
        name = vm.get_name()
        if name.startswith("guestfs-"):  # pragma: no cover
            log.debug("ignore libvirt/guestfs temporary VM %s", name)
            return

        self._q.put((conn.get_uri(), vm.get_name()))

    def _start(self):
        self._thread = threading.Thread(name="inspection thread", target=self._run)
        self._thread.daemon = True
        self._thread.start()

    def _stop(self):
        if self._thread is None:
            return

        self._q.put(None)
        self._thread = None

    def _run(self):
        # Process everything on the queue.  If the queue is empty when
        # called, block.
        while True:
            data = self._q.get()
            if data is None:
                log.debug("libguestfs queue vm=None, exiting thread")
                return
            uri, vmname = data
            self._process_vm(uri, vmname)
            self._q.task_done()

    def _process_vm(self, uri, vmname):
        connmanager = vmmConnectionManager.get_instance()
        conn = connmanager.conns.get(uri)
        if not conn:
            return  # pragma: no cover

        vm = conn.get_vm_by_name(vmname)
        if not vm:
            return  # pragma: no cover

        # Try processing a single VM, keeping into account whether it was
        # visited already, and whether there are cached data for it.
        def _set_vm_inspection_data(_data):
            vm.set_inspection_data(_data)
            self._cached_data[vm.get_uuid()] = _data

        prettyvm = conn.get_uri() + ":" + vm.get_name()
        vmuuid = vm.get_uuid()
        if vmuuid in self._cached_data:
            data = self._cached_data.get(vmuuid)
            if vm.inspection != data:
                log.debug("Found cached data for %s", prettyvm)
                _set_vm_inspection_data(data)
            return

        try:
            data = self._inspect_vm(conn, vm)
        except Exception as e:  # pragma: no cover
            data = _inspection_error(_("Error inspection VM: %s") % str(e))
            log.exception("%s: exception while processing", prettyvm)

        _set_vm_inspection_data(data)

    def _inspect_vm(self, conn, vm):
        if self._thread is None:
            return  # pragma: no cover

        if conn.is_remote():  # pragma: no cover
            return _inspection_error(_("Cannot inspect VM on remote connection"))
        if conn.is_test():
            return _make_fake_data(vm)

        return _perform_inspection(conn, vm)  # pragma: no cover

    ##############
    # Public API #
    ##############

    def vm_refresh(self, vm):
        log.debug("Refresh requested for vm=%s", vm.get_name())

        # When refreshing the inspection data of a VM,
        # all we need is to remove it from the "seen" cache,
        # as the data itself will be replaced once the new
        # results are available.
        self._cached_data.pop(vm.get_uuid(), None)
        self._q.put((vm.conn.get_uri(), vm.get_name()))
