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

import configparser
import os
import re

from ..logger import log
from ..osdict import OSDB


###############################################
# Helpers for detecting distro from given URL #
###############################################

class _DistroCache(object):
    def __init__(self, fetcher):
        self._fetcher = fetcher
        self._filecache = {}

        self._treeinfo = None
        self.treeinfo_family = None
        self.treeinfo_version = None
        self.treeinfo_name = None
        self.treeinfo_matched = False

        self.suse_content = None
        self.checked_for_suse_content = False
        self.debian_media_type = None
        self.mageia_version = None

        self.libosinfo_os_variant = None
        self.libosinfo_mediaobj = None
        self.libosinfo_treeobj = None

    def acquire_file_content(self, path):
        if path not in self._filecache:
            try:
                content = self._fetcher.acquireFileContent(path)
            except ValueError:
                content = None
                log.debug("Failed to acquire file=%s", path)
            self._filecache[path] = content
        return self._filecache[path]

    @property
    def treeinfo(self):
        if self._treeinfo:
            return self._treeinfo

        # Vast majority of trees here use .treeinfo. However, trees via
        # Red Hat satellite on akamai CDN will use treeinfo, because akamai
        # doesn't do dotfiles apparently:
        #
        #   https://bugzilla.redhat.com/show_bug.cgi?id=635065
        #
        # Anaconda is the canonical treeinfo consumer and they check for both
        # locations, so we need to do the same
        treeinfostr = (self.acquire_file_content(".treeinfo") or
            self.acquire_file_content("treeinfo"))
        if treeinfostr is None:
            return None

        # If the file doesn't parse or there's no 'family', this will
        # error, but that should be fine because we aren't going to
        # successfully detect the tree anyways
        treeinfo = configparser.ConfigParser()
        treeinfo.read_string(treeinfostr)
        self.treeinfo_family = treeinfo.get("general", "family")
        self._treeinfo = treeinfo
        log.debug("treeinfo family=%s", self.treeinfo_family)

        if self._treeinfo.has_option("general", "version"):
            self.treeinfo_version = self._treeinfo.get("general", "version")
            log.debug("Found treeinfo version=%s", self.treeinfo_version)

        if self._treeinfo.has_option("general", "name"):
            self.treeinfo_name = self._treeinfo.get("general", "name")
            log.debug("Found treeinfo name=%s", self.treeinfo_name)

        return self._treeinfo

    def treeinfo_family_regex(self, famregex):
        if not self.treeinfo:
            return False

        ret = bool(re.match(famregex, self.treeinfo_family))
        self.treeinfo_matched = ret
        if not ret:
            log.debug("Didn't match treeinfo family regex=%s", famregex)
        return ret

    def content_regex(self, filename, regex):
        """
        Fetch 'filename' and return True/False if it matches the regex
        """
        content = self.acquire_file_content(filename)
        if content is None:
            return False

        for line in content.splitlines():
            if re.match(regex, line):
                return True

        log.debug("found filename=%s but regex=%s didn't match",
                filename, regex)
        return False

    def get_treeinfo_media(self, typ):
        """
        Pull kernel/initrd/boot.iso paths out of the treeinfo for
        the passed data
        """
        def _get_treeinfo_path(media_name):
            image_type = self.treeinfo.get("general", "arch")
            if typ == "xen":
                image_type = "xen"
            return self.treeinfo.get("images-%s" % image_type, media_name)

        try:
            return [(_get_treeinfo_path("kernel"),
                     _get_treeinfo_path("initrd"))]
        except Exception:  # pragma: no cover
            log.debug("Failed to parse treeinfo kernel/initrd",
                    exc_info=True)
            return []

    def split_version(self):
        verstr = self.treeinfo_version
        def _safeint(c):
            try:
                return int(c)
            except Exception:
                return 0

        # Parse a string like 6.9 or 7.4 into its two parts
        # centos altarch's have just version=7
        update = 0
        version = _safeint(verstr)
        if verstr.count(".") >= 1:
            version = _safeint(verstr.split(".")[0])
            update = _safeint(verstr.split(".")[1])

        log.debug("converted verstr=%s to version=%s update=%s",
                verstr, version, update)
        return version, update

    def fetcher_is_iso(self):
        return self._fetcher.is_iso()

    def guess_os_from_iso(self):
        ret = OSDB.guess_os_by_iso(self._fetcher.location)
        if not ret:
            return False

        self.libosinfo_os_variant, self.libosinfo_mediaobj = ret
        if (not self.libosinfo_mediaobj.get_kernel_path() or
            not self.libosinfo_mediaobj.get_initrd_path()):  # pragma: no cover
            # This can happen if the media is live media, or just
            # with incomplete libosinfo data
            log.debug("libosinfo didn't report any media kernel/initrd "
                          "path for detected os_variant=%s",
                          self.libosinfo_mediaobj)
            return False
        return True

    def guess_os_from_tree(self):
        ret = OSDB.guess_os_by_tree(self._fetcher.location)
        if not ret:
            return False

        self.libosinfo_os_variant, self.libosinfo_treeobj = ret
        self.treeinfo_matched = True
        return True


def getDistroStore(guest, fetcher, skip_error):
    log.debug("Finding distro store for location=%s", fetcher.location)

    arch = guest.os.arch
    _type = guest.os.os_type
    osobj = guest.osinfo
    stores = _build_distro_list(osobj)
    cache = _DistroCache(fetcher)

    for sclass in stores:
        if not sclass.is_valid(cache):
            continue

        store = sclass(fetcher.location, arch, _type, cache)
        log.debug("Detected class=%s osvariant=%s",
                      store.__class__.__name__, store.get_osdict_info())
        return store

    if skip_error:
        return None

    # No distro was detected. See if the URL even resolves, and if not
    # give the user a hint that maybe they mistyped. This won't always
    # be true since some webservers don't allow directory listing.
    # https://www.redhat.com/archives/virt-tools-list/2014-December/msg00048.html
    extramsg = ""
    if not fetcher.can_access():
        extramsg = (": " +
            _("The URL could not be accessed, maybe you mistyped?"))

    msg = (_("Could not find an installable distribution at URL '%s'") %
            fetcher.location)
    msg += extramsg
    msg += "\n\n"
    msg += _("The location must be the root directory of an install tree.\n"
          "See virt-install man page for various distro examples.")
    raise ValueError(msg)


##################
# Distro classes #
##################

class _DistroTree(object):
    """
    Class for determining the kernel/initrd path for an install
    tree (URL, ISO, or local directory)
    """
    PRETTY_NAME = None
    matching_distros = []

    def __init__(self, location, arch, vmtype, cache):
        self.type = vmtype
        self.arch = arch
        self.uri = location
        self.cache = cache

        if self.cache.libosinfo_os_variant:
            self._os_variant = self.cache.libosinfo_os_variant
        else:
            self._os_variant = self._detect_version()

        if (self._os_variant and
            not OSDB.lookup_os(self._os_variant)):
            log.debug("Detected os_variant as %s, which is not in osdict.",
                    self._os_variant)
            self._os_variant = None

        self._kernel_paths = []
        if self.cache.treeinfo_matched:
            self._kernel_paths = self.cache.get_treeinfo_media(self.type)
        else:
            self._set_manual_kernel_paths()


    def _set_manual_kernel_paths(self):
        """
        If kernel/initrd path could not be determined from a source
        like treeinfo, subclasses can override this to set a list
        of manual paths
        """

    def _detect_version(self):
        """
        Hook for subclasses to detect media os variant.
        """
        log.debug("%s does not implement any osdict detection", self)
        return None


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

    @classmethod
    def is_valid(cls, cache):
        raise NotImplementedError

    def get_kernel_paths(self):
        return self._kernel_paths

    def get_osdict_info(self):
        """
        Return detected osdict value
        """
        return self._os_variant

    def get_os_media(self):
        """
        Return an OsMedia wrapper around the detected libosinfo media object
        """
        return self.cache.libosinfo_mediaobj

    def get_os_tree(self):
        """
        Return an OsTree wrapper around the detected libosinfo media object
        """
        return self.cache.libosinfo_treeobj


class _GenericTreeinfoDistro(_DistroTree):
    """
    Generic catchall class for .treeinfo using distros
    """
    PRETTY_NAME = "Generic Treeinfo"
    matching_distros = []

    @classmethod
    def is_valid(cls, cache):
        if cache.treeinfo:
            cache.treeinfo_matched = True
            return True
        return False


class _LibosinfoDistro(_DistroTree):
    """
    For ISO media detection that was fully handled by libosinfo
    """
    PRETTY_NAME = "Libosinfo detected"
    matching_distros = []

    @classmethod
    def is_valid(cls, cache):
        if cache.fetcher_is_iso():
            return cache.guess_os_from_iso()
        return cache.guess_os_from_tree()

    def _set_manual_kernel_paths(self):
        self._kernel_paths += [
                (self.cache.libosinfo_mediaobj.get_kernel_path(),
                 self.cache.libosinfo_mediaobj.get_initrd_path())
        ]


def _build_distro_list(osobj):
    allstores = [
        # Libosinfo takes priority
        _LibosinfoDistro,
        # Always stick GenericDistro at the end, since it's a catchall
        _GenericTreeinfoDistro,
    ]

    # If user manually specified an os_distro, bump its URL class
    # to the top of the list
    if osobj.distro:
        log.debug("variant=%s has distro=%s, looking for matching "
                      "distro store to prioritize",
                      osobj.name, osobj.distro)
        found_store = None
        for store in allstores:
            if osobj.distro in store.matching_distros:
                found_store = store

        if found_store:
            log.debug("Prioritizing distro store=%s", found_store)
            allstores.remove(found_store)
            allstores.insert(0, found_store)
        else:
            log.debug("No matching store found, not prioritizing anything")

    force_libosinfo = os.environ.get("VIRTINST_TEST_SUITE_FORCE_LIBOSINFO")
    if force_libosinfo:  # pragma: no cover
        if bool(int(force_libosinfo)):
            allstores = [_LibosinfoDistro]
        else:
            allstores.remove(_LibosinfoDistro)

    return allstores
