#!/usr/bin/python2
# -*- mode: python; coding: utf-8 -*-
#
# Copyright 2021, Cendio AB.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import sys
import os
import re
import pickle

import logging
import warnings

try:
    import yum
    import rpmUtils
    from rpmUtils import arch
except ImportError:
    sys.exit("Could not import YUM API")

def syntax():
    sys.stderr.write("Syntax: \n")
    sys.stderr.write("    %s <command> [args]\n" % sys.argv[0])
    sys.exit(1)

def do_test():
    # Don't need to do anything
    pass

def _is_i686_arch():
    arch = os.uname()[4]
    if re.match("i.86", arch):
        return True
    else:
        return False

def _prepare_transaction(base, pkgs):
    all_good = True
    base.doLoggingSetup(2, 2)

    # Remove all internal handlers and ensure propagation of message
    # to the root logger
    yl = ["yum","yum.verbose", "yum.Depsolve", "yum.verbose.Depsolve",
          "yum.YumBase","yum.verbose.YumBase",
          "yum.plugin","yum.verbose.plugin",
          "yum.verbose.YumPlugins", "yum.Repos",
          "yum.verbose.Repos", "yum.RepoStorage",
          "yum-filelogging.RPMInstallCallback", "yum.filelogger"]
    for l in yl:
        logger = logging.getLogger(l)
        logger.handlers = []
        logger.propagate = True

    warnings.simplefilter("ignore", DeprecationWarning)
    warnings.simplefilter("ignore", PendingDeprecationWarning)

    try:
        # get a list of compatible archs for system basearch
        # special handling of I686 which can involve several
        # 32bit archs
        if _is_i686_arch():
            compatArchList = rpmUtils.arch.getArchList(None)
        else:
            compatArchList = [rpmUtils.arch.getBaseArch(), "noarch"]

        # Update package lists
        pl = base.doPackageLists("all")
        if pl is None:
            raise Exception("failed to get package list")

        for name in pkgs:
            pkg = None
            logging.info("resolving package '%s' for installation." % name)

            # check if package already is installed then skip to next package
            # in list of packages wanted to be installed
            candidates = [name]

            # Don't forget about virtual packages
            result = base.returnPackagesByDep(name)
            if len(result) != 0:
                for r in result:
                    if r.arch not in compatArchList:
                        continue
                    candidates.append(r.name)

            installed = None
            for c in candidates:
                iem, im, ium = yum.packages.parsePackages(pl.installed, [c])
                result = yum.misc.unique(iem)
                for r in result:
                    if r.arch not in compatArchList:
                        continue
                    installed = r
                    break
                if installed is not None:
                    break
            if installed is not None:
                logging.warning("package '%s' is already installed (as '%s')" % (name, installed.name))
                continue

            # check if package is available for update/installation
            aem, am, aum = yum.packages.parsePackages(pl.available, [name])
            result = yum.misc.unique(aem)
            if len(result) != 0:
                for r in result:
                    if r.arch in compatArchList:
                        pkg = r
                        break

            # if we havent got any package at this point check if its virtual provide
            if pkg is None:
                result = base.returnPackagesByDep(name)
                candidates = []
                for r in result:
                    if len(candidates) > 0 and candidates[0].name != r.name:
                        continue
                    if r.arch in compatArchList:
                        candidates.append(r)
                if len(candidates) > 0:
                    pkg = yum.misc.newestInList(candidates)[0]
                    logging.info("package '%(virt)s' provided by package '%(pkg)s'" %
                                 {"virt":name, "pkg":pkg.name})

            if pkg is not None:
                # Package is available for installation/update, add it to packages
                # and flag it for installation
                base.install(po=pkg)
                logging.info("package '%s' flagged for installation" % str(pkg))
            else:
                logging.warning("no package named '%s' found" % name)
                all_good = False


        # build transaction resolves dependencies then get each
        # transaction member that is to be installed / updated
        base.buildTransaction()

        # let yum analyse the transaction and build handy
        # lists of packages in neat categories
        base.tsInfo.makelists(True, True)

        # FIXME
        assert len(base.tsInfo.removed) == 0
        assert len(base.tsInfo.depremoved) == 0
    except Exception, e:
        logging.critical("failed to resolve depends with reason: %s" % str(e))
        all_good = False

    if not all_good:
        sys.exit(1)


def _convert_pkgs(pkgs):
    output = []
    for pkg in pkgs:
        output.append({"name": pkg.name,
                       "version": "%s-%s" % (pkg.version, pkg.release),
                       "arch": pkg.arch})
    return output

def do_resolve(pkgs):
    base = yum.YumBase()
    _prepare_transaction(base, pkgs)

    resolved = base.tsInfo.installed + \
                        base.tsInfo.updated + \
                        base.tsInfo.reinstalled + \
                        base.tsInfo.downgraded
    depends = base.tsInfo.depinstalled + \
                       base.tsInfo.depupdated

    resolved = _convert_pkgs(resolved)
    depends = _convert_pkgs(depends)

    data = { "resolved": resolved,
             "depends": depends}
    # We need to use version 2 so that Python 2.x can read this
    sys.stdout.write(pickle.dumps(data, protocol=2))


class YumProcessTransBaseCallback(yum.callbacks.ProcessTransBaseCallback):
    def __init__(self):
        yum.callbacks.ProcessTransBaseCallback.__init__(self)

    def event(self, state, data=None):
        message = None
        if state == yum.callbacks.PT_DOWNLOAD:
            message = yum.callbacks.PT_MESSAGES[yum.callbacks.PT_DOWNLOAD]
        elif state == yum.callbacks.PT_DOWNLOAD_PKGS:
            pass
        elif state == yum.callbacks.PT_GPGCHECK:
            message = yum.callbacks.PT_MESSAGES[yum.callbacks.PT_GPGCHECK]
        elif state == yum.callbacks.PT_TEST_TRANS:
            message = yum.callbacks.PT_MESSAGES[yum.callbacks.PT_TEST_TRANS]
        elif state == yum.callbacks.PT_TRANSACTION:
            message = yum.callbacks.PT_MESSAGES[yum.callbacks.PT_TRANSACTION]

        if message is not None:
            sys.stderr.write("ONMESSAGE: %s\n" % message)
            sys.stderr.flush()

class YumRPMBaseCallback(yum.rpmtrans.RPMBaseCallback):
    def __init__(self):
        yum.rpmtrans.RPMBaseCallback.__init__(self)

    def event(self, package, action, te_current, te_total, ts_current, ts_total):
        action_string = self.__action_to_string(action)

        sys.stderr.write("ONPROGRESS: %s %s %d %d %d %d\n" %
                         (action_string, str(package), te_current, te_total, ts_current, ts_total))
        sys.stderr.flush()

    def __action_to_string(self, action):
        act = None
        if action == yum.constants.TS_UPDATE:
            act = "Updating"
        elif action == yum.constants.TS_ERASE:
            act = "Erasing"
        elif action == yum.constants.TS_INSTALL:
            act = "Installing"
        elif action == yum.constants.TS_TRUEINSTALL:
            act = "Installing"
        elif action == yum.constants.TS_OBSOLETED:
            act = "Obsoleted"
        elif action == yum.constants.TS_OBSOLETING:
            act = "Obsoleting"
        elif action == yum.constants.TS_UPDATED:
            act = "Updated"

        if act is None:
            act = "UnknownAction"

        return act

def do_install(pkgs):
    base = yum.YumBase()
    _prepare_transaction(base, pkgs)
    if len(base.tsInfo) == 0:
        logging.warning("  no packages available for install")
        sys.exit("  no packages available for install")

    yum_rpm_callback = YumRPMBaseCallback()

    yum_trans_callback = YumProcessTransBaseCallback()

    try:
        base.processTransaction(callback=yum_trans_callback,
                                rpmDisplay=yum_rpm_callback)

    except Exception, e:
        sys.exit("  failed to install packages with reason: %s" % str(e))


def main():
    if len(sys.argv) < 2:
        syntax()

    logging.basicConfig(format='%(message)s', level=logging.DEBUG)

    try:
        if sys.argv[1] == "test":
            do_test()
        elif sys.argv[1] == "resolve":
            do_resolve(sys.argv[2:])
        elif sys.argv[1] == "install":
            do_install(sys.argv[2:])
        else:
            sys.exit("Unknown command \"%s\"" % sys.argv[1])
    except Exception as e:
        sys.exit('YUM error: %s' % e)

    sys.exit(0)

if __name__ == "__main__":
    main()
