#
# Copyright 2006-2009, 2013, 2014 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

import os

from . import urldetect
from . import urlfetcher
from .installerinject import perform_initrd_injections
from .. import progress
from ..devices import DeviceDisk
from ..logger import log
from ..osdict import OSDB


# Enum of the various install media types we can have
(MEDIA_DIR, MEDIA_ISO, MEDIA_URL, MEDIA_KERNEL) = range(1, 5)


def _is_url(url):
    return url.startswith("http://") or url.startswith("https://") or url.startswith("ftp://")


class _LocationData:
    def __init__(self, osinfo, kernel_pairs, os_media, os_tree):
        self.osinfo = osinfo
        self.kernel_pairs = kernel_pairs
        self.os_media = os_media
        self.os_tree = os_tree

        self.kernel_url_arg = None
        if self.osinfo:
            osobj = OSDB.lookup_os(self.osinfo)
            self.kernel_url_arg = osobj.get_kernel_url_arg()


class InstallerTreeMedia:
    """
    Class representing --location Tree media. Can be one of

      - A network URL: http://dl.fedoraproject.org/...
      - A local directory
      - A local .iso file, which will be accessed with isoinfo
    """

    @staticmethod
    def validate_path(conn, path):
        try:
            dev = DeviceDisk(conn)
            dev.device = dev.DEVICE_CDROM
            dev.set_source_path(path)
            dev.validate()
            return dev.get_source_path()
        except Exception as e:
            log.debug("Error validating install location", exc_info=True)
            if path.startswith("nfs:"):
                log.warning(
                    "NFS URL installs are no longer supported. "
                    "Access your install media over an alternate transport "
                    "like HTTP, or manually mount the NFS share and install "
                    "from the local directory mount point."
                )

            msg = _("Validating install media '%(media)s' failed: %(error)s") % {
                "media": str(path),
                "error": str(e),
            }
            raise ValueError(msg) from None

    @staticmethod
    def get_system_scratchdir(guest):
        """
        Return the tmpdir that's accessible by VMs on system libvirt URIs
        """
        if guest.conn.is_xen():
            return "/var/lib/xen"
        return "/var/lib/libvirt/boot"

    @staticmethod
    def make_scratchdir(guest):
        """
        Determine the scratchdir for this URI, create it if necessary.
        scratchdir is the directory that's accessible by VMs
        """
        user_scratchdir = os.path.join(guest.conn.get_app_cache_dir(), "boot")
        system_scratchdir = InstallerTreeMedia.get_system_scratchdir(guest)

        # If we are a session URI, or we don't have access to the system
        # scratchdir, make sure the session scratchdir exists and use that.
        if (
            guest.conn.is_unprivileged()
            or not os.path.exists(system_scratchdir)
            or not os.access(system_scratchdir, os.W_OK)
        ):
            os.makedirs(user_scratchdir, 0o751, exist_ok=True)
            return user_scratchdir

        return system_scratchdir  # pragma: no cover

    def __init__(
        self,
        conn,
        location,
        location_kernel,
        location_initrd,
        install_kernel,
        install_initrd,
        install_kernel_args,
    ):
        self.conn = conn
        self.location = location
        self._location_kernel = location_kernel
        self._location_initrd = location_initrd
        self._install_kernel = install_kernel
        self._install_initrd = install_initrd
        self._install_kernel_args = install_kernel_args
        self._initrd_injections = []
        self._extra_args = []

        if location_kernel or location_initrd:
            if not location:
                raise ValueError(
                    _("location kernel/initrd may only be specified with a location URL/path")
                )
            if not (location_kernel and location_initrd):
                raise ValueError(_("location kernel/initrd must be specified as a pair"))

        self._cached_fetcher = None
        self._cached_data = None

        self._tmpfiles = []

        if self._install_kernel or self._install_initrd:
            self._media_type = MEDIA_KERNEL
        elif (
            not self.conn.is_remote()
            and os.path.exists(self.location)
            and os.path.isdir(self.location)
        ):
            self.location = os.path.abspath(self.location)
            self._media_type = MEDIA_DIR
        elif _is_url(self.location):
            self._media_type = MEDIA_URL
        else:
            self._media_type = MEDIA_ISO

        if (
            self.conn.is_remote()
            and not self._media_type == MEDIA_URL
            and not self._media_type == MEDIA_KERNEL
        ):
            raise ValueError(
                _("Cannot access install tree on remote connection: %s") % self.location
            )

        if self._media_type == MEDIA_ISO:
            InstallerTreeMedia.validate_path(self.conn, self.location)

    ########################
    # Install preparations #
    ########################

    def _get_fetcher(self, guest, meter):
        meter = progress.ensure_meter(meter)

        if not self._cached_fetcher:
            scratchdir = InstallerTreeMedia.make_scratchdir(guest)

            if self._media_type == MEDIA_KERNEL:
                self._cached_fetcher = urlfetcher.DirectFetcher(None, scratchdir, meter)
            else:
                self._cached_fetcher = urlfetcher.fetcherForURI(self.location, scratchdir, meter)

        self._cached_fetcher.meter = meter
        return self._cached_fetcher

    def _get_cached_data(self, guest, fetcher):
        if self._cached_data:
            return self._cached_data

        store = None
        osinfo = None
        os_media = None
        os_tree = None
        kernel_paths = []
        has_location_kernel = bool(self._location_kernel and self._location_initrd)

        if self._media_type == MEDIA_KERNEL:
            kernel_paths = [(self._install_kernel, self._install_initrd)]
        else:
            store = urldetect.getDistroStore(guest, fetcher, skip_error=has_location_kernel)

        if store:
            kernel_paths = store.get_kernel_paths()
            osinfo = store.get_osdict_info()
            os_media = store.get_os_media()
            os_tree = store.get_os_tree()
        if has_location_kernel:
            kernel_paths = [(self._location_kernel, self._location_initrd)]

        self._cached_data = _LocationData(osinfo, kernel_paths, os_media, os_tree)
        return self._cached_data

    def _prepare_kernel_url(self, guest, cache, fetcher):
        ignore = guest

        def _check_kernel_pairs():
            for kpath, ipath in cache.kernel_pairs:
                if fetcher.hasFile(kpath) and fetcher.hasFile(ipath):
                    return kpath, ipath
            raise RuntimeError(_("Couldn't find kernel for install tree."))  # pragma: no cover

        kernelpath, initrdpath = _check_kernel_pairs()
        kernel = fetcher.acquireFile(kernelpath)
        self._tmpfiles.append(kernel)
        initrd = fetcher.acquireFile(initrdpath)
        self._tmpfiles.append(initrd)

        perform_initrd_injections(initrd, self._initrd_injections, fetcher.scratchdir)

        return kernel, initrd

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

    def _prepare_unattended_data(self, scripts):
        if not scripts:
            return

        for script in scripts:
            expected_filename = script.get_expected_filename()
            scriptpath = script.write()
            self._tmpfiles.append(scriptpath)
            self._initrd_injections.append((scriptpath, expected_filename))

    def _prepare_kernel_url_arg(self, guest, cache):
        osinfo = cache.osinfo or guest.osinfo.name
        osobj = OSDB.lookup_os(osinfo)
        return osobj.get_kernel_url_arg()

    def _prepare_kernel_args(self, guest, cache, unattended_scripts):
        install_args = None
        if unattended_scripts:
            args = []
            for unattended_script in unattended_scripts:
                cmdline = unattended_script.generate_cmdline()
                if cmdline:
                    args.append(cmdline)
            install_args = (" ").join(args)
            log.debug("Generated unattended cmdline: %s", install_args)
        elif self.is_network_url():
            kernel_url_arg = self._prepare_kernel_url_arg(guest, cache)
            if kernel_url_arg:
                install_args = "%s=%s" % (kernel_url_arg, self.location)

        if install_args:
            self._extra_args.append(install_args)

        if self._install_kernel_args:
            ret = self._install_kernel_args
        else:
            ret = " ".join(self._extra_args)

        if self._media_type == MEDIA_DIR and not ret:
            log.warning(
                _(
                    "Directory tree installs typically do not work "
                    "unless extra kernel args are passed to point the "
                    "installer at a network accessible install tree."
                )
            )
        return ret

    def prepare(self, guest, meter, unattended_scripts):
        fetcher = self._get_fetcher(guest, meter)
        cache = self._get_cached_data(guest, fetcher)

        self._prepare_unattended_data(unattended_scripts)
        kernel_args = self._prepare_kernel_args(guest, cache, unattended_scripts)

        kernel, initrd = self._prepare_kernel_url(guest, cache, fetcher)
        return kernel, initrd, kernel_args

    def cleanup(self, guest):
        ignore = guest
        for f in self._tmpfiles:
            log.debug("Removing %s", str(f))
            os.unlink(f)

        self._tmpfiles = []

    def set_initrd_injections(self, initrd_injections):
        self._initrd_injections = initrd_injections

    def set_extra_args(self, extra_args):
        self._extra_args = extra_args

    def cdrom_path(self):
        if self._media_type in [MEDIA_ISO]:
            return self.location

    def is_network_url(self):
        if self._media_type in [MEDIA_URL]:
            return self.location

    def detect_distro(self, guest):
        fetcher = self._get_fetcher(guest, None)
        cache = self._get_cached_data(guest, fetcher)
        return cache.osinfo

    def get_os_media(self, guest, meter):
        fetcher = self._get_fetcher(guest, meter)
        cache = self._get_cached_data(guest, fetcher)
        return cache.os_media

    def get_os_tree(self, guest, meter):
        fetcher = self._get_fetcher(guest, meter)
        cache = self._get_cached_data(guest, fetcher)
        return cache.os_tree

    def requires_internet(self, guest, meter):
        if self._media_type in [MEDIA_URL, MEDIA_DIR]:
            return True

        os_media = self.get_os_media(guest, meter)
        if os_media:
            return os_media.is_netinst()
        return False
