#!/usr/bin/python
#
# Clonse channels by a particular date
#
# Copyright (c) 2008--2012 Red Hat, Inc.
#
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

import re
import sys
import datetime
import getpass
import os
import StringIO
from optparse import OptionParser
import simplejson as json

_LIBPATH = "/usr/share/rhn"
if _LIBPATH not in sys.path:
    sys.path.append(_LIBPATH)

from utils import cloneByDate
from utils.cloneByDate import UserError


SAMPLE_CONFIG = """
{
 "username":"admin",
 "password":"redhat",
 "assumeyes":true,
 "to_date": "2011-10-01",
 "skip_depsolve":false,
 "security_only":false,
 "use_update_date":false,
 "no_errata_sync":false,
 "blacklist": {
                 "ALL":["sendmail"],
                 "my-rhel5-x86_64-clone":["emacs"],
                 "my-rhel5-i386-clone":["vi", "postfix.*"]
              },
 "removelist": {
                 "ALL":["compiz", "compiz-gnome"],
                 "my-rhel5-x86_64-clone":["boost.*"]
              },
 "channels":[
             {
                "rhel-x86_64-server-5":"my-rhel5-x86_64-clone",
                "rhn-tools-rhel-x86_64-server-5": "my-tools-5-x86_64-clone"
             },
            {
                "rhel-i386-server-5": "my-rhel5-i386-clone"
             }
           ]
}
"""


def merge_config(options):
    if options.channels:
        options.channels = transform_arg_channels(options.channels)
        return options
    elif not options.config:
        return options

    if not os.path.isfile(options.config):
        raise UserError("%s does not exist." % options.config)

    try:
        config_file = open(options.config).read()
        # strip any and all whitespace
        config_file = re.sub(r'\s', '', config_file)
        config = json.load(StringIO.StringIO(config_file))
    except:
        raise UserError("Configuration file is invalid, please check syntax.")

    #if soemthing is in the config and not passed in as an argument
    #   add it to options
    overwrite = ["username", "password", "blacklist", "removelist", "channels",
                 "server", "assumeyes", "to_date", "skip_depsolve",
                 "security_only", "use_update_date", "no_errata_sync"]
    for key in overwrite:
        if config.has_key(key) and not getattr(options, key):
            setattr(options, key, config[key])

    if type(options.channels) == dict:
        options.channels =  [options.channels]

    validate_list_dict("blacklist", options.blacklist)
    validate_list_dict("removelist", options.removelist)

    return options



def validate_list_dict(name, pkg_dict):
    """
        Validates a removelist or blacklist to be map with lists as values
    """
    if type(pkg_dict) != type({}):
        raise UserError("%s  is not formatted correctly" % name)
    for key, value in pkg_dict.items():
        if type(value) != type([]):
            raise UserError("Channel %s in %s packages not formatted correctly" % (key, name))

# Using --channels as an argument only supports a single channel 'tree'
#  So we need to convert a 2-tuple list of channel labels into an array with a hash
#  ex:   [ ("rhel-i386-servr-5", "my-rhel-clone"), ('rhel-child', 'clone-child')]
#    should become
# [{
#  "rhel-i386-servr-5" : "my-rhel-clone",
#  'rhel-child': 'clone-child'
#  }]
def transform_arg_channels(chan_list):
    to_ret = {}
    for src, dest in chan_list:
        to_ret[src] = dest
    return [to_ret]

def parse_args():
    parser = OptionParser()
    parser.add_option("-c", "--config", dest="config", help="Config file specifying options")
    parser.add_option("-u", "--username", dest="username", help="Username")
    parser.add_option("-p", "--password", dest="password", help="Password")
    parser.add_option("-s", "--server", dest="server", help="Server URL to use for api connections (defaults to https://localhost/rpc/api)", default="https://localhost/rpc/api")
    parser.add_option("-l", "--channels", dest="channels", nargs=2, action="append", help="Original channel and clone channel labels space separated (e.g. --channels=rhel-i386-server-5 myclone).  Can be specified multiple times.")
    parser.add_option("-b", "--blacklist", dest="blacklist",  help="Comma separated list of package names (or regular expressions) to exclude from cloned errata (Only added packages will be considered).")
    parser.add_option("-r", "--removelist", dest="removelist",  help="Comma separated list of package names (or regular expressions) to remove from destination channel (All packages are available for removal).")
    parser.add_option("-d", "--to_date", dest="to_date", help="Clone errata to the specified date (YYYY-MM-DD). If omitted will assume no errata.")
    parser.add_option("-y", "--assumeyes", dest='assumeyes', action='store_true', help="Assume yes for any prompts (unattended).")
    parser.add_option("-m", "--sample-config", dest='sample', action='store_true', help="Print a sample full configuration file and exit.")
    parser.add_option("-k", "--skip_depsolve", dest='skip_depsolve', action='store_true', help="Skip all dependency solving (Not recommended).")
    parser.add_option("-v", "--validate", dest='validate', action='store_true', help="Run repoclosure on the set of specified repositories.")
    parser.add_option("-g", "--background", dest='background', action='store_true', help="Clone the errata in the background. Prompt will return quicker; before cloning is finished.")
    parser.add_option("-o", "--security_only", dest='security_only', action='store_true', help="Only clone security errata (and their dependencies).")
    # Note: the parents option has no logical analog for the config
    # file. This option is provided primarily as a convenience option to
    # provide a more intuitive way to clone a child channels for an
    # already-existing parent. However, the config file can easily
    # contain many different channel trees, and any implementation that
    # involves multiple channel trees would be extremely un-intuitive.
    # Config file users must re-list the parent channels, the tool
    # correctly determins if a channel already exists and does not try
    # to re-create it.
    parser.add_option("-a", "--parents", dest="parents", nargs=2,
            help="Already existing channels that will be used as the "
            + "original parent and destination parent of child "
            + "channels cloned this session.")
    parser.add_option("-z", "--use-update-date", dest="use_update_date", action='store_true', help="While cloning errata by date, clone all errata that have last been updated on or before the date provided by to_date. If omitted will use issue date of errata (default).")
    parser.add_option("-n", "--no-errata-sync", dest="no_errata_sync", action='store_true', help="Do not automatically sychronize the package list of cloned errata with their originals. This may make spacewalk-clone-by-date have unexpected results if the original errata have been updated (e.g.: syncing another architecture for a channel) since the cloned errata were created. If omitted we will synchronize the cloned errata with the originals to ensure the expected packages are included (default).")

    (options, args) = parser.parse_args()

    if options.parents != None:
        if (len(options.parents) != 2
                or options.parents[0].startswith('-')
                or options.parents[1].startswith('-')):
            raise UserError("The -a / --parents option requires two arguments")

    # have to check this option before we merge with the config file to
    # ensure that optparse is parsing the args correctly. We have to
    # check it again after the config file merge to make sure we have
    # channels.
    if options.channels != None:
        for channel_group in options.channels:
            if (len(channel_group) != 2 or channel_group[0].startswith('-')
                    or channel_group[1].startswith('-')):
                raise UserError("The -l / --channels option requires two arguments")

    if options.sample:
        print SAMPLE_CONFIG
        sys.exit(0)

    if options.config and options.channels:
        raise UserError("Cannot specify both --channels and --config.")

    if options.config and options.parents:
        raise UserError("Cannot specify both --parents and --config.")

    if options.blacklist:
        options.blacklist = {"ALL":options.blacklist.split(",")}

    if options.removelist:
        options.removelist = {"ALL":options.removelist.split(",")}

    options = merge_config(options)

    if options.channels == None or len(options.channels) == 0:
        raise UserError("No channels specified. See --help for details.")

    if not options.username:
        raise UserError("Username not specified")

    if not options.validate:
        if options.to_date:
            options.to_date = parse_time(options.to_date)

    if not options.password:
        options.password = getpass.getpass()


    return options


def parse_time(time_str):
    """
     We need to use datetime, but python 2.4 does not support strptime(), so we have to parse ourselves
    """
    if re.match('[0-9]{4}-[0-9]{2}-[0-9]{2}',time_str):
        try:
            split = time_str.split("-")
            date = datetime.datetime(int(split[0]), int(split[1]), int(split[2]))
        except:
            raise UserError("Invalid date (%s)" % time_str)
        return date
    else:
        raise UserError("Invalid date format (%s), expected YYYY-MM-DD" % time_str)

def systemExit(code, msgs=None):
    """
     Exit with a code and optional message(s). Saved a few lines of code.
    """
    if msgs:
        if type(msgs) not in [type([]), type(())]:
            msgs = (msgs, )
        for msg in msgs:
            sys.stderr.write(str(msg)+'\n')
    sys.exit(code)

def main():
    try:
        args = parse_args();
        return cloneByDate.main(args)
    except KeyboardInterrupt:
        systemExit(0, "\nUser interrupted process.")
    except UserError, error:
        systemExit(-1, "\n%s" % error)
    return 0


if __name__ == '__main__':
    try:
        sys.exit(abs(main() or 0))
    except KeyboardInterrupt:
        systemExit(0, "\nUser interrupted process.")
