#!/usr/bin/python3
from __future__ import print_function
import sys
import os
import shutil
import logging
import subprocess
from spacewalk.server import rhnSQL
from spacewalk.common import usix
from spacewalk.common.rhnConfig import CFG, initCFG
from optparse import OptionParser
import textwrap
from string import Template

try:
   input = raw_input
except NameError:
   pass

msg_wrapper = textwrap.TextWrapper()
msg_wrapper.initial_indent = '- '
msg_wrapper.width = 75
msg_wrapper.subsequent_indent = '  '

sys.path.append("/usr/share/susemanager")

initCFG('server.susemanager')
rhnSQL.initDB()

basepath = CFG.MOUNT_POINT or '/var/spacewalk'

_sql_synced_proucts = rhnSQL.Statement("""
SELECT distinct sp.product_id as id
FROM suseProducts sp
JOIN suseProductChannel spc ON sp.id = spc.product_id
WHERE spc.channel_id IS NOT NULL
""")

_sql_find_pkgs = """
SELECT distinct
        pkg.id AS id,
        PN.name || '-' || evr_t_as_vre_simple(full_list.evr) || '.' || full_list.arch_label AS nvrea,
        full_list.arch_label AS arch,
        pkg.path
  FROM  (
         SELECT  I_P.name_id name_id,
                 max(I_PE.evr) evr,
                 I_PA.id as arch_id,
                 I_PA.label as arch_label,
                 (CASE WHEN channels.parent_channel IS NULL THEN channels.id ELSE channels.parent_channel END) as root
           FROM  (
                  select I_C.*
                   from suseProducts I_SP
                   JOIN suseProductChannel I_PC ON I_SP.id = I_PC.product_id
                   JOIN rhnChannel I_C ON I_PC.channel_id = I_C.id
                  WHERE  I_SP.product_id in ( %s )
                  union
                  select *
                    from rhnChannel
                   where org_id is not null
                     and parent_channel in (
                                            select pc.id
                                              from rhnChannel pc
                                             where pc.label = :parentchannel)
                  ) channels
           JOIN  rhnChannelNewestPackage I_CNP ON channels.id = I_CNP.channel_id
           JOIN  rhnPackage I_P ON I_CNP.package_id = I_P.id
           JOIN  rhnPackageEVR I_PE ON I_P.evr_id = I_PE.id
           JOIN  rhnPackageArch I_PA ON I_P.package_arch_id = I_PA.id
       GROUP BY  I_P.name_id, I_PA.label, I_PA.id, root
     ) full_list,
       rhnPackage pkg
       join rhnPackageName pn on pkg.name_id = pn.id
       join rhnPackageEVR pevr on pkg.evr_id = pevr.id
       join rhnChannelPackage CP on CP.package_id = pkg.id
       join rhnChannel c on CP.channel_id = c.id
 WHERE full_list.name_id = pkg.name_id
   AND full_list.evr = pevr.evr
   AND full_list.arch_id = pkg.package_arch_id
   AND (c.parent_channel = full_list.root OR c.id = full_list.root)
   AND pn.name = :pkgname
order by pkg.id
""";

_sql_find_pkgs_custom = """
SELECT distinct
        pkg.id AS id,
        PN.name || '-' || evr_t_as_vre_simple(full_list.evr) || '.' || full_list.arch_label AS nvrea,
        full_list.arch_label AS arch,
        pkg.path
  FROM  (
         SELECT  I_P.name_id name_id,
                 max(I_PE.evr) evr,
                 I_PA.id as arch_id,
                 I_PA.label as arch_label,
                 (CASE WHEN channels.parent_channel IS NULL THEN channels.id ELSE channels.parent_channel END) as root
           FROM  (
                  select *
                    from rhnChannel
                   where org_id is not null
                     and label = :parentchannel
                      or parent_channel in (
                                            select pc.id
                                              from rhnChannel pc
                                             where pc.label = :parentchannel)
                  ) channels
           JOIN  rhnChannelNewestPackage I_CNP ON channels.id = I_CNP.channel_id
           JOIN  rhnPackage I_P ON I_CNP.package_id = I_P.id
           JOIN  rhnPackageEVR I_PE ON I_P.evr_id = I_PE.id
           JOIN  rhnPackageArch I_PA ON I_P.package_arch_id = I_PA.id
       GROUP BY  I_P.name_id, I_PA.label, I_PA.id, root
     ) full_list,
       rhnPackage pkg
       join rhnPackageName pn on pkg.name_id = pn.id
       join rhnPackageEVR pevr on pkg.evr_id = pevr.id
       join rhnChannelPackage CP on CP.package_id = pkg.id
       join rhnChannel c on CP.channel_id = c.id
 WHERE full_list.name_id = pkg.name_id
   AND full_list.evr = pevr.evr
   AND full_list.arch_id = pkg.package_arch_id
   AND (c.parent_channel = full_list.root OR c.id = full_list.root)
   AND pn.name = :pkgname
order by pkg.id
""";


_sql_find_root_channel_label = """
select label
from rhnChannel
where id in (
  select distinct c.parent_channel
    from rhnChannel c
    join suseProductChannel pc on pc.channel_id = c.id
    join suseProducts p on pc.product_id = p.id
   WHERE p.product_id in ( {0} )
)
""";

_sql_find_custom_parent_channel_labels = """
select label
from rhnChannel
where parent_channel IS NULL
and org_id IS NOT NULL
""";

def find_root_channel_labels(pdids):
    """
    Get root channel labels for selected distribution

    :return: list of root channel labels
    """
    h = rhnSQL.Statement( _sql_find_root_channel_label.format(pdids))
    return [x['label'] for x in rhnSQL.fetchall_dict(h) or []]

def find_custom_parent_channel_labels():
    """
    Get custom parent channel labels

    :return: list of custom parent channel labels
    """
    h = rhnSQL.Statement(_sql_find_custom_parent_channel_labels)
    return [x['label'] for x in rhnSQL.fetchall_dict(h) or []]


def list_labels():
    """
    Create list of labels and return a structure of them for the menu.

    :return:
    """
    label_map = {}
    synced_products = [x['id'] for x in rhnSQL.fetchall_dict(_sql_synced_proucts) or []]
    custom_parent_channels = find_custom_parent_channel_labels()
    label_index = 1
    for label in sorted(mgr_bootstrap_data.DATA.keys()):
        if 'PDID' in mgr_bootstrap_data.DATA[label] and int(mgr_bootstrap_data.DATA[label]['PDID'][0]) in synced_products:
            print("{0}. {1}".format(label_index, label))
            label_map[label_index] = label
            label_index += 1
        elif 'BASECHANNEL' in mgr_bootstrap_data.DATA[label] and mgr_bootstrap_data.DATA[label]['BASECHANNEL'] in custom_parent_channels:
            print("{0}. {1}".format(label_index, label))
            label_map[label_index] = label
            label_index += 1
    return label_map

def create_repo(label, flush, additional=[]):
    pdids = None
    if 'PDID' in mgr_bootstrap_data.DATA[label]:
        pdids = ', '.join(mgr_bootstrap_data.DATA[label]['PDID'])
    else:
        options.usecustomchannels = True
        options.parentchannel = mgr_bootstrap_data.DATA[label]['BASECHANNEL']

    destdir = mgr_bootstrap_data.DATA[label]['DEST']
    errors = 0
    messages = []
    suggestions = {
        'no-packages': None
    }

    if options.usecustomchannels:
        if pdids:
            root_labels = find_root_channel_labels(pdids)
        else:
            root_labels = find_custom_parent_channel_labels()
        if options.parentchannel and options.parentchannel not in root_labels:
            print("'{0}' not found in existing parent channel options '{1}'".format(options.parentchannel, root_labels))
            sys.exit(1)
        elif not options.parentchannel:
            if len(root_labels) > 1:
                print("Multiple options for parent channel found. Please use option --with-parent-channel <label>")
                print("and choose one of:")
                for l in root_labels:
                    print("- {0}".format(l))
                sys.exit(1)
            elif len(root_labels) == 1:
                options.parentchannel = root_labels[0]
            else:
                print("WARNING: no parent channel found. Execute without using custom channels")
                options.parentchannel = ""
    else:
        options.parentchannel = ""

    if flush:
        print("Removing directory:", destdir)
        try:
            shutil.rmtree(destdir)
        except OSError as err:
            print('Error while deleting {0}: {1}'.format(destdir, err))

    if not os.path.exists(destdir):
        if dryrun:
            print("Create directory:", destdir)
        else:
            os.makedirs(destdir)

    if label.startswith('RES') or label.startswith('RHEL'):
        print("Creating bootstrap repo for latest Service Pack of {0}".format(label))
    else:
        print("Creating bootstrap repo for {0}".format(label))
    print()

    if pdids:
        h = rhnSQL.prepare(rhnSQL.Statement(_sql_find_pkgs % (pdids)))
    else:
        h = rhnSQL.prepare(rhnSQL.Statement(_sql_find_pkgs_custom ))
    packagelist = mgr_bootstrap_data.DATA[label]['PKGLIST']
    repotype = mgr_bootstrap_data.DATA[label].get('TYPE', 'yum')
    packagelist.extend(additional)
    logging.debug("The bootstrap repo should contain the following packages: {0}".format(packagelist))

    debs_dir = os.path.join(destdir, 'debs')
    for pkgaltstr in packagelist:
        alt = pkgaltstr.split("|")
        altpretty =  ', '.join("'%s'" %(e) for e in alt)
        altcount = 0
        for pkgname in alt:
            altcount += 1
            h.execute(parentchannel=options.parentchannel, pkgname=pkgname)
            pkgs = h.fetchall_dict() or []
            logging.debug("Package {0} found {1} resulting packages:".format(
                pkgname, len(pkgs)))
            if len(pkgs) == 0:
                if altcount >= len(alt):
                    if len(alt) > 1:
                        messages.append("ERROR: none of %s found" % altpretty)
                    else:
                        messages.append("ERROR: package '%s' not found" % pkgname)
                    errors += 1
                    if not suggestions['no-packages']:
                        suggestions['no-packages'] = ('mgr-create-bootstrap-repo uses the locally synchronized ' +
                                                      'versions of files from the Tools repository, ' +
                                                      'and uses the locally synchronized pool channel for dependency resolution. ' +
                                                      'Both should be fully synced before running the mgr-create-bootstrap-repo script.')
                continue
            for p in pkgs:
                logging.debug(p)
                rpmdir = os.path.join(destdir, p['arch']) if repotype != 'deb' else debs_dir
                if not os.path.exists(rpmdir):
                    os.makedirs(rpmdir)
                print("copy '%s'" % p['nvrea'])
                logging.debug("copy {0} / {1} to {2}".format(basepath, p['path'], rpmdir))
                if not dryrun:
                    shutil.copy2(os.path.join(basepath, p['path']), rpmdir)
            break

    if dryrun:
        if repotype == 'deb':
            debs = " ".join([os.path.join(debs_dir, f) for f in os.listdir(debs_dir)])
            print("/usr/bin/reprepro -b {0} includedeb {1} {2}".format(destdir, "bootstrap", debs))
        else:
            print("createrepo -s sha %s" % destdir)
    else:
        if repotype == 'deb':
            reprepro_conf_tmpl = Template("Origin: $origin\n" \
            "Label: $label\n" \
            "Codename: $codename\n" \
            "Architectures: $arches\n" \
            "Components: $comps\n" \
            "Description: $desc\n")
            codename="bootstrap"
            reprepro_conf = reprepro_conf_tmpl.substitute(origin="mgr",
                    label="mgr", codename=codename, arches="amd64 i386", comps="main", desc="Bootstrap repo")
            reprepro_conf_dir = os.path.join(destdir, "conf")
            if not os.path.exists(reprepro_conf_dir):
                os.makedirs(reprepro_conf_dir)
            with open(os.path.join(reprepro_conf_dir, "distributions"), "w") as conf_file:
                conf_file.write(reprepro_conf)
            logging.debug("Created reprepro config file in {0}".format(os.path.join(destdir, "distributions")))
            debs = [os.path.join(debs_dir, f) for f in os.listdir(debs_dir)]
            try:
                subprocess.run(["/usr/bin/reprepro", "-b", destdir, "includedeb", codename] + debs, check=True)
                logging.debug("Removing directory {0}".format(debs_dir))
                shutil.rmtree(debs_dir, ignore_errors=True)
            except subprocess.CalledProcessError as err:
                print("Error creating bootstrap repo.")
                logging.debug(err)
                sys.exit(1)
        else:    
            os.system("createrepo -s sha %s" % destdir)
    if errors:
        for m in messages:
            print(m)
        if (label.startswith('RES') or label.startswith('RHEL') or label.lower().startswith('ubuntu'))  and not options.usecustomchannels:
            print("If the installation media was imported into a custom channel, try to run again with --with-custom-channels option")
        suggestions = list([_f for _f in list(suggestions.values()) if _f])
        if suggestions:
            print("\nSuggestions:")
            for suggestion in suggestions:
                print(msg_wrapper.fill(suggestion) + "\n")
        sys.exit(1)

usage = "usage: %prog [options] [additional_pkg1 additional_pkg2 ...]"
parser = OptionParser(usage=usage)
parser.add_option('-n', '--dryrun', action='store_true', dest='dryrun',
                  help='Dry run. Show only changes - do not execute them')
parser.add_option('-i', '--interactive', action='store_true', dest='interactive',
                  help='Interactive mode (default)')
parser.add_option('-l', '--list', action='store_true', dest='list',
                  help='list available distributions')
parser.add_option('-c', '--create', action='store', dest='create',
                  help='create bootstrap repo for given distribution label')
parser.add_option('-f', '--flush', action='store_true', dest='flush',
                  help='when used in conjuction with --create, deletes the target repository before creating it')
parser.add_option('', '--datamodule', action="store", dest='datamodule',
                  help='Use an own datamodule (Default: mgr_bootstrap_data)')
parser.add_option('', '--with-custom-channels', action='store_true', dest='usecustomchannels',
                  help='Take custom channels into account when searching for newest package versions')
parser.add_option('', '--with-parent-channel', action="store", dest='parentchannel',
                  help='use child channels below this parent')
parser.add_option('-d', '--debug', action='store_true', dest='debug',
                  help='Enable debug mode')

(options, args) = parser.parse_args()

if options.debug:
    logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)

modulename = "mgr_bootstrap_data"
if options.datamodule and options.datamodule != '':
    modulename = options.datamodule

try:
    mgr_bootstrap_data = __import__(modulename)
    for label in mgr_bootstrap_data.DATA:
        if 'PDID' in mgr_bootstrap_data.DATA[label]:
            if not isinstance(mgr_bootstrap_data.DATA[label]['PDID'], usix.ListType):
                mgr_bootstrap_data.DATA[label]['PDID'] = list(map(str, [int(mgr_bootstrap_data.DATA[label]['PDID'])]))
            else:
                mgr_bootstrap_data.DATA[label]['PDID'] = list(map(str, [int(pdid) for pdid in mgr_bootstrap_data.DATA[label]['PDID']]))

except ImportError as e:
    sys.stderr.write("Unable to load module '%s'\n" % modulename)
    sys.stderr.write(str(e) + "\n")
    sys.exit(1)

dryrun = options.dryrun

if not options.list and not options.create:
    options.interactive = True

if options.interactive:
    label_map = list_labels()

    elabel = None
    while True:
        try:
            elabel = label_map.get(int(input("Enter a number of a product label: ")), '')
            break
        except Exception:
            print("Please enter a number.")

    if elabel not in mgr_bootstrap_data.DATA:
        print("'%s' not found" % elabel)
        sys.exit(1)

    create_repo(elabel, options.flush, additional=args)
elif options.list:
    list_labels()
elif options.create:
    if options.create not in mgr_bootstrap_data.DATA:
        print("'%s' not found" % options.create)
        sys.exit(1)
    create_repo(options.create, options.flush, additional=args)
