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

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 map(lambda x: x['label'], 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 map(lambda x: x['label'], 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'):
        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']
    packagelist.extend(additional)
    logging.debug("The bootstrap repo should contain the following packages: {0}".format(packagelist))

    for pkgname in packagelist:
        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:
            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 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)

    if dryrun:
        print("createrepo -s sha %s" % destdir)
    else:
        os.system("createrepo -s sha %s" % destdir)
    if errors:
        for m in messages:
            print(m)
        if label.startswith('RES') 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)
