#!/usr/bin/env python3
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

#
# NOTE: this actually needs to run under python2.4 and centos 5.x through python3 and centos 7.x
# please manually test changes on centos5 boxes or you will almost certainly break things.
#

import sys
import yum
import signal
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'simplejson'))
try: import json
except ImportError: import simplejson as json
import re
from rpmUtils.miscutils import stringToVersion,compareEVR
from rpmUtils.arch import getBaseArch, getArchList


try: from yum.misc import string_to_prco_tuple
except ImportError:
    # RHEL5 compat
    def string_to_prco_tuple(prcoString):
        prco_split = prcoString.split()
        n, f, v = prco_split
        (prco_e, prco_v, prco_r) = stringToVersion(v)
        return (n, f, (prco_e, prco_v, prco_r))

# hack to work around https://github.com/chef/chef/issues/7126
# see https://bugzilla.redhat.com/show_bug.cgi?id=1396248
if not hasattr(yum.packages.FakeRepository, 'compare_providers_priority'):
    yum.packages.FakeRepository.compare_providers_priority = 99

base = None

def get_base():
    global base
    if base is None:
        base = yum.YumBase()
    setup_exit_handler()
    return base

def versioncompare(versions):
    arch_list = getArchList()
    candidate_arch1 = versions[0].split(".")[-1]
    candidate_arch2 = versions[1].split(".")[-1]

    # The first version number passed to this method is always a valid nevra (the current version)
    # If the second version number looks like it does not contain a valid arch
    # then we'll chop the arch component (assuming it *is* a valid one) from the first version string
    # so we're only comparing the evr portions.
    if (candidate_arch2 not in arch_list) and (candidate_arch1 in arch_list):
       final_version1 = versions[0].replace("." + candidate_arch1,"")
    else:
       final_version1 = versions[0]

    final_version2 = versions[1]

    (e1, v1, r1) = stringToVersion(final_version1)
    (e2, v2, r2) = stringToVersion(final_version2)

    evr_comparison = compareEVR((e1, v1, r1), (e2, v2, r2))
    outpipe.write("%(e)s\n" % { 'e': evr_comparison })
    outpipe.flush()

def install_only_packages(name):
    base = get_base()
    if name in base.conf.installonlypkgs:
      outpipe.write('True')
    else:
      outpipe.write('False')
    outpipe.flush()

# python2.4 / centos5 compat
try:
    any
except NameError:
    def any(s):
        for v in s:
            if v:
                return True
        return False

def query(command):
    base = get_base()

    enabled_repos = base.repos.listEnabled()

    # Handle any repocontrols passed in with our options

    if 'repos' in command:
      for repo in command['repos']:
        if 'enable' in repo:
          base.repos.enableRepo(repo['enable'])
        if 'disable' in repo:
          base.repos.disableRepo(repo['disable'])

    args = { 'name': command['provides'] }
    do_nevra = False
    if 'epoch' in command:
        args['epoch'] = command['epoch']
        do_nevra = True
    if 'version' in command:
        args['ver'] = command['version']
        do_nevra = True
    if 'release' in command:
        args['rel'] = command['release']
        do_nevra = True
    if 'arch' in command:
        desired_arch = command['arch']
        args['arch'] = command['arch']
        do_nevra = True
    else:
        desired_arch = getBaseArch()

    obj = None
    if command['action'] == "whatinstalled":
        obj = base.rpmdb
    else:
        obj = base.pkgSack

    # if we are given "name == 1.2.3" then we must use the getProvides() API.
    #   - this means that we ignore arch and version properties when given prco tuples as a package_name
    #   - in order to fix this, something would have to happen where getProvides was called first and
    #     then the result was searchNevra'd.  please be extremely careful if attempting to fix that
    #     since searchNevra does not support prco tuples.
    if bool(re.search('\\s+', command['provides'])):
        # handles flags (<, >, =, etc) and versions, but no wildcareds
        # raises error for any invalid input like: 'FOO BAR BAZ' 
        pkgs = obj.getProvides(*string_to_prco_tuple(command['provides']))
    elif do_nevra:
        # now if we're given version or arch properties explicitly, then we do a SearchNevra.
        #  - this means that wildcard version in the package_name with an arch property will not work correctly
        #  - again don't try to fix this just by pushing bugs around in the code, you would need to call
        #    returnPackages and searchProvides and then apply the Nevra filters to those results.
        pkgs = obj.searchNevra(**args)
        if (command['action'] == "whatinstalled") and (not pkgs):
          pkgs = obj.searchNevra(name=args['name'], arch=desired_arch)
    else:
        pats = [command['provides']]
        pkgs = obj.returnPackages(patterns=pats)

        if not pkgs:
            # handles wildcards
            pkgs = obj.searchProvides(command['provides'])

    if not pkgs:
        outpipe.write(command['provides'].split().pop(0)+' nil nil\n')
        outpipe.flush()
    else:
        # make sure we picked the package with the highest version
        pkgs = base.bestPackagesFromList(pkgs,single_name=True)
        pkg = pkgs.pop(0)
        outpipe.write("%(n)s %(e)s:%(v)s-%(r)s %(a)s\n" % { 'n': pkg.name, 'e': pkg.epoch, 'v': pkg.version, 'r': pkg.release, 'a': pkg.arch })
        outpipe.flush()

    # Reset any repos we were passed in enablerepo/disablerepo to the original state in enabled_repos
    if 'repos' in command:
      for repo in command['repos']:
        if 'enable' in repo:
          if base.repos.getRepo(repo['enable']) not in enabled_repos:
            base.repos.disableRepo(repo['enable'])
        if 'disable' in repo:
          if base.repos.getRepo(repo['disable']) in enabled_repos:
            base.repos.enableRepo(repo['disable'])

# the design of this helper is that it should try to be 'brittle' and fail hard and exit in order
# to keep process tables clean.  additional error handling should probably be added to the retry loop
# on the ruby side.
def exit_handler(signal, frame):
    if base is not None:
        base.closeRpmDB()
    sys.exit(0)

def setup_exit_handler():
    signal.signal(signal.SIGINT, exit_handler)
    signal.signal(signal.SIGHUP, exit_handler)
    signal.signal(signal.SIGPIPE, exit_handler)
    signal.signal(signal.SIGQUIT, exit_handler)

if len(sys.argv) < 3:
  inpipe = sys.stdin
  outpipe = sys.stdout
else:
  inpipe = os.fdopen(int(sys.argv[1]), "r")
  outpipe = os.fdopen(int(sys.argv[2]), "w")

try:
    while 1:
        # kill self if we get orphaned (tragic)
        ppid = os.getppid()
        if ppid == 1:
            raise RuntimeError("orphaned")

        setup_exit_handler()
        line = inpipe.readline()

        try:
            command = json.loads(line)
        except ValueError, e:
            raise RuntimeError("bad json parse")

        if command['action'] == "whatinstalled":
            query(command)
        elif command['action'] == "whatavailable":
            query(command)
        elif command['action'] == "versioncompare":
            versioncompare(command['versions'])
        elif command['action'] == "installonlypkgs":
             install_only_packages(command['package'])
        else:
            raise RuntimeError("bad command")
finally:
    if base is not None:
        base.closeRpmDB()
