# coding=utf-8
# Licensed Materials - Property of IBM
# Copyright IBM Corp. 2019

import sys
import sysconfig
import os
import argparse
import requests
import warnings
import urllib3
import datetime
import json
import locale
import re

import streamsx.topology.context
from streamsx.rest import Instance
from streamsx.build import BuildService



###########################################
# submitjob
###########################################
def _submitjob_parser(subparsers):
    job_submit = subparsers.add_parser('submitjob', help='Submit an application bundle')
    job_submit.add_argument('sabfile', help='Location of sab file.', metavar='sab-pathname')
    job_submit.add_argument('--jobConfig', '-g', help='Specifies the name of an external file that defines a job configuration overlay', metavar='file-name')
    job_submit.add_argument('--jobname', help='Specifies the name of the job.', metavar='job-name')
    job_submit.add_argument('--jobgroup', '-J', help='Specifies the job group', metavar='jobgroup-name')
    job_submit.add_argument('--outfile', help='Specifies the path and file name of the output file in which the command writes the list of submitted job IDs', metavar='file-name')
    job_submit.add_argument('--P', '-P', help='Specifies a submission-time parameter and value for the job', action='append', metavar='parameter-name')
    _user_arg(job_submit)

def _submitjob(instance, cmd_args, rc):
    """Submit a job."""
    job_config = None

    if cmd_args.jobConfig:
        with open(cmd_args.jobConfig) as fd:
            job_config = streamsx.topology.context.JobConfig.from_overlays(json.load(fd))
    else:
        job_config = streamsx.topology.context.JobConfig()

    if cmd_args.jobname:
        job_config.job_name = cmd_args.jobname

    if cmd_args.jobgroup:
        # Will throw a 500 internal error on instance.submit_job if jobgroup doesn't already exist
        job_config.job_group = cmd_args.jobgroup

    if cmd_args.P:
        for param in cmd_args.P:
            name_value_pair = param.split("=")
            if len(name_value_pair) != 2:
                raise ValueError("The format of the following submission-time parameter is not valid: {}. The correct syntax is: <name>=<value>".format(param))
            else:
                job_config.submission_parameters[name_value_pair[0]] = name_value_pair[1]

    job = instance.submit_job(bundle=cmd_args.sabfile, job_config=job_config)

    if job:
        print("The following number of applications were submitted to the {} instance: {}.".format(instance.id, 1))
        print("Submitted job IDs: {}".format(job.id), file=sys.stderr)
    else:
        raise Exception("Error in creating Job")

    # If --outfile, write jobID to file
    if cmd_args.outfile:
        with open(cmd_args.outfile, 'w') as my_file:
            my_file.write(str(job.id) + '\n')

    return (rc, job)

###########################################
# canceljob
###########################################
def _canceljob_parser(subparsers):
    job_cancel = subparsers.add_parser('canceljob', help='Cancel a job.')
    job_cancel.add_argument('--force', action='store_true', help='Stop the service even if jobs are running.', default=False)
    job_cancel.add_argument('--collectlogs', help='Specifies to collect the log and trace files for each processing element that is associated with the job', action='store_true')
    job_cancel.add_argument('jobid', help='Specifies a list of job IDs.', nargs='*', metavar='jobid')
    # Only 1 of these arguments --jobs, --jobnames, --file can be specified at any given time when running this command
    g1 = job_cancel.add_argument_group(title='jobs jobnames file group', description='One of these options must be chosen.')
    group = g1.add_mutually_exclusive_group(required=False)
    group.add_argument('--jobs', '-j', help='Specifies a list of job IDs.', metavar='job-id')
    group.add_argument('--jobnames', help='Specifies a list of job names', metavar='job-names')
    group.add_argument('--file', '-f', help='Specifies the file that contains a list of job IDs, one per line', metavar='file-name')

    _user_arg(job_cancel)

def _canceljob(instance, cmd_args, rc):
    """Cancel a job."""
    job_ids_to_cancel = []
    job_names_to_cancel = []

    # Check for mutually exclusive arguments, just in case argparse doesn't catch it
    temp = len(list(filter(None, [cmd_args.jobid, cmd_args.jobs, cmd_args.jobnames, cmd_args.file])))
    if temp > 1:
        raise ValueError("Arguments jobid, --jobs, --jobnames, --file are mutually exclusive")

    # get list of job IDs to cancel
    if cmd_args.jobid:
        for x in cmd_args.jobid:
            job_ids = x.split(',')
            job_ids = [job.strip() for job in job_ids]
            job_ids_to_cancel.extend(job_ids)

    # if --jobs, get list of job IDs to cancel
    if cmd_args.jobs:
        job_ids = cmd_args.jobs.split(',')
        job_ids_to_cancel.extend(job_ids)

    # if --jobnames, get list of jobnames to cancel
    if cmd_args.jobnames:
        job_names = cmd_args.jobnames.split(',')
        job_names_to_cancel.extend(job_names)

    # if --file, get list of job IDs to cancel from file
    if cmd_args.file:
        with open(cmd_args.file, 'r') as my_file:
            job_ids = [line.rstrip() for line in my_file if not line.isspace()]
            job_ids_to_cancel.extend(job_ids)

    # Filter out any blank/empty strings in job_ids_to_cancel and job_names_to_cancel
    # Ex. 'canceljob job1.id , job2.id' -> job_ids_to_cancel = ['job1.id', '', '', 'job2.id'], should be = ['job1.id', 'job2.id']
    job_ids_to_cancel = list(filter(None, job_ids_to_cancel))
    job_names_to_cancel = list(filter(None, job_names_to_cancel))

    # If no jobs to cancel, raise error
    if not job_ids_to_cancel and not job_names_to_cancel:
        raise Exception("No jobs provided")

    # Check if job w/ job ID exists, and if so cancel it
    for x in job_ids_to_cancel:
        if x.isnumeric():
            try:
                job = instance.get_job(id=str(x))
                _job_cancel(instance, x, cmd_args.collectlogs, cmd_args.force)
            except:
                print("The following job ID was not found {}".format(x), file=sys.stderr)
                print("The following job ID cannot be canceled: {}. See the previous error message".format(x), file=sys.stderr)
                rc = 1
        else:
            raise ValueError("The following job identifier is not valid: {}. Specify a job identifier that is numeric and try the request again.".format(x))

    # Check if job w/ job name exists, and if so cancel it
    for x in job_names_to_cancel:
        # if jobname contains a space, its invalid
        if ' ' in x:
            print("{} is not a valid job name. Either its size is longer than 1024 characters or it includes some invalid characters.".format(x), file=sys.stderr)
            rc = 1
            continue
        jobs = instance.get_jobs(name=str(x))
        if jobs:
            job = jobs[0]
            _job_cancel(instance, job.id, cmd_args.collectlogs, cmd_args.force)
        else:
            print("The following job name is not found: {}. Specify a job name that is valid and try the request again.".format(x), file=sys.stderr)
            rc = 1

    return (rc, None)

def _job_cancel(instance, job_id=None, collectlogs=False, force=False):
    job = instance.get_job(id=str(job_id))
    if collectlogs:
        log_path = job.retrieve_log_trace()
        if log_path:
            print("The log files for the {} job ID will be collected in the following files: {}".format(job_id, log_path))
        else:
            raise Exception("Retrieval of job's logs is not supported in this version of IBM Streams")

    # Cancel job, and check if successful
    val = job.cancel(force)
    if val:
        print("The following job ID was canceled: {}. The job was in the {} instance.".format(job_id, instance.id))
    else:
        raise Exception("One or more jobs failed to stop")


###########################################
# lsjobs
###########################################
def _lsjobs_parser(subparsers):
    job_ls = subparsers.add_parser('lsjobs', help='List the jobs for a given instance')
    job_ls.add_argument('--jobs', '-j', help='Specifies a list of job IDs.', metavar='job-id')
    job_ls.add_argument('--users', '-u', help='Specifies to select from this list of user IDs', metavar='user')
    job_ls.add_argument('--jobnames', help='Specifies a list of job names', metavar='job-names')
    job_ls.add_argument('--fmt', help='Specifies the presentation format', default='%Tf', metavar='format-spec')
    job_ls.add_argument('--xheaders', help='Specifies to exclude headings from the report', action='store_true')
    job_ls.add_argument('--long', '-l', help='Reports launch count, full host names, and all of the operator instance names for the PEs.', action='store_true')
    job_ls.add_argument('--showtimestamp', help='Specifies to show a time stamp in the output to indicate when the command was run.', action='store_true')

    _user_arg(job_ls)

def _lsjobs(instance, cmd_args, rc):
    """view jobs"""
    jobs = instance.get_jobs()
    instance_id = instance.id

    # Omit header if xheaders is present
    if cmd_args.xheaders:
        cmd_args.showtimestamp = False
        instance_id = None

    # If --users argument (ie given list of user ID's), filter jobs by these user ID's
    if (cmd_args.users):
        users = cmd_args.users.split(",")
        jobs = [job for job in jobs if job.startedBy in users]

    # If --jobs argument (ie given list of job ID's), filter jobs by these ID's
    if (cmd_args.jobs):
        job_ids = cmd_args.jobs.split(",")
        jobs = [job for job in jobs if job.id in job_ids]

    # If --jobsnames argument (ie given list of job names), filter jobs by these user job names
    if (cmd_args.jobnames):
        job_names = cmd_args.jobnames.split(",")
        jobs = [job for job in jobs if job.name in job_names]

    # If --showtimestamp, give current date
    time_stamp = None
    LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
    if cmd_args.showtimestamp:
        time_stamp = datetime.datetime.now().replace(microsecond=0).replace(tzinfo=LOCAL_TIMEZONE).isoformat()

    headers = ["Id", "State", "Healthy", "User", "Date", "Name", "Group"]

    # If --long option, add productVersion to the headers and get the corresponding value for each job
    if cmd_args.long:
        headers.append('ProductVersion')

    # pre process the data so output is formatted nicely
    h_length = [len(x) for x in headers] # header_length
    job_data = []
    for job in jobs:
        jobHealth = "yes" if job.health == "healthy" else "no"
        jobTime = datetime.datetime.fromtimestamp(job.submitTime/1000, tz = LOCAL_TIMEZONE).isoformat() # job.submitTime/1000 to convert ms to sec
        jobGroup = job.jobGroup.split("/")[-1]
        data = [job.id, job.status.capitalize(), jobHealth, job.startedBy, jobTime, job.name, jobGroup]

        h_length[0] = max(len(job.id), h_length[0])
        h_length[1] = max(len(job.status), h_length[1])
        h_length[2] = max(len(jobHealth), h_length[2])
        h_length[3] = max(len(job.startedBy), h_length[3])
        h_length[4] = max(len(jobTime), h_length[4])
        h_length[5] = max(len(job.name), h_length[5])
        h_length[6] = max(len(jobGroup), h_length[6])

        # If --long option, add productVersion to the job data
        if cmd_args.long:
            prod_version = job.productVersion
            h_length[7] = max(len(prod_version), h_length[7])
            data.append(prod_version)
        job_data.append(data)

    # Output the data

    # If default output format
    if cmd_args.fmt == '%Tf':
        if time_stamp:
            print("Date: " + time_stamp)

        if instance_id:
            print("Instance: " + instance_id)
            # String that represents the header w/ formatting for nice output
            header_string = '{:>{id_w}}  {:<{state_w}}  {:<{health_w}}  {:<{user_w}}  {:<{date_w}}  {:<{name_w}}  {:<{group_w}}'.format(
                "Id", "State", "Healthy", "User", "Date", "Name", "Group",
                id_w=h_length[0], state_w=h_length[1],
                health_w=h_length[2], user_w=h_length[3],
                date_w=h_length[4], name_w=h_length[5], group_w=h_length[6])

            # If --long option, we also want the header to display ProductVersion
            if cmd_args.long:
                header_string += '  {:<{prod_w}}'.format("ProductVersion", prod_w=h_length[7])
            print(header_string)

        for job in job_data:
            # String that represents a job w/ formatting for nice output
            job_string = '{:>{id_w}}  {:<{state_w}}  {:<{health_w}}  {:<{user_w}}  {:<{date_w}}  {:<{name_w}}  {:<{group_w}}'.format(
                job[0], job[1], job[2], job[3], job[4], job[5], job[6],
                id_w=h_length[0], state_w=h_length[1],
                health_w=h_length[2], user_w=h_length[3],
                date_w=h_length[4], name_w=h_length[5], group_w=h_length[6])

            # If --long option, we also want the job to display ProductVersion
            if cmd_args.long:
                job_string += '  {:<{prod_w}}'.format(job[7], prod_w=h_length[7])
            print(job_string)

    elif cmd_args.fmt == "%Mf":
        border = '=' * 50

        if instance_id:
            print(border)
            if time_stamp:
                print("Date: " + time_stamp)
            print("Instance: " + instance_id)

        w1 = len(max(headers, key=len))
        w2 = max(h_length)
        for job in job_data:
            print(border)
            for header, job_item in zip(headers, job):
                print('{:{w1}}  :  {: <{w2}}'.format(header, job_item, w1=w1, w2=w2))
        print(border)

    elif cmd_args.fmt == "%Nf":
        for job in job_data:
            toPrint = ''
            for header, row in zip(headers, job):
                toPrint += '{}  :  {} '.format(header, row)
            print(toPrint)

    return (rc, None)

###########################################
# appconfig
###########################################

# ls-appconfig
def _lsappconfig_parser(subparsers):
    appconfig_ls = subparsers.add_parser('lsappconfig', help='Retrieve a list of configurations for making a connection to an external application')
    appconfig_ls.add_argument('--fmt', help='Specifies the presentation format', default='%Tf', metavar='format-spec')
    _user_arg(appconfig_ls)

def _lsappconfig(instance, cmd_args, rc):
    """view appconfigs"""
    configs = instance.get_application_configurations()
    locale.setlocale(locale.LC_ALL, '') # Needed to correctly display local date and time format
    LOCAL_TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo # Needed to get timezone

    headers = ["Id", "Owner", "Created", "Modified", "Description"]

    # pre process the data so output is formatted nicely
    h_length = [len(x) for x in headers] # header_length
    config_data = []
    for config in configs:
        createDate = datetime.datetime.fromtimestamp(config.creationTime/1000, tz = LOCAL_TIMEZONE).strftime("%x %X %Z")
        lastModifiedDate = datetime.datetime.fromtimestamp(config.lastModifiedTime/1000, tz = LOCAL_TIMEZONE).strftime("%x %X %Z")

        data = [config.name, config.owner, createDate, lastModifiedDate, config.description]
        config_data.append(data)

        h_length[0] = max(len(config.name), h_length[0])
        h_length[1] = max(len(config.owner), h_length[1])
        h_length[2] = max(len(createDate), h_length[2])
        h_length[3] = max(len(lastModifiedDate), h_length[3])
        h_length[4] = max(len(config.description), h_length[4])

    # Output the data

    # If default output format
    if cmd_args.fmt == '%Tf':
        print('{:>{id_w}}  {:<{owner_w}}  {:<{created_w}}  {:<{mod_w}}  {:<{desc_w}}'.format(
            "Id", "Owner", "Created", "Modified", "Description",
            id_w=h_length[0], owner_w=h_length[1],
            created_w=h_length[2], mod_w=h_length[3],
            desc_w=h_length[4]))

        for config in config_data:
            print('{:>{id_w}}  {:<{owner_w}}  {:<{created_w}}  {:<{mod_w}}  {:<{desc_w}}'.format(
            config[0], config[1], config[2], config[3], config[4],
            id_w=h_length[0], owner_w=h_length[1],
            created_w=h_length[2], mod_w=h_length[3],
            desc_w=h_length[4]))

    elif cmd_args.fmt == "%Mf":
        border = '=' * 50

        w1 = len(max(headers, key=len))
        w2 = max(h_length)
        for config in config_data:
            print(border)
            for header, config_item in zip(headers, config):
                print('{:{w1}}  :  {: <{w2}}'.format(header, config_item, w1=w1, w2=w2))
        print(border)

    elif cmd_args.fmt == "%Nf":
        for config in config_data:
            toPrint = ''
            for header, row in zip(headers, config):
                toPrint += '{}  :  {} '.format(header, row)
            print(toPrint)

    return (rc, None)

# rm-appconfig
def _rmappconfig_parser(subparsers):
    appconfig_rm = subparsers.add_parser('rmappconfig', help='Removes a configuration that is used for making a connection to an external application')
    appconfig_rm.add_argument('config_name', help='Name of the app config', metavar='config-name')
    appconfig_rm.add_argument('--noprompt', help='Specifies to suppress confirmation prompts.', action='store_true')
    _user_arg(appconfig_rm)

def _rmappconfig(instance, cmd_args, rc):
    """remove an appconfig"""
    config_name = cmd_args.config_name
    configs = instance.get_application_configurations(name = config_name)
    if (not configs):
        raise NameError("The {} application configuration does not exist in the {} instance".format(config_name, instance.id))
    app_config = instance.get_application_configurations(name = config_name)[0]

    # No confirmation required, delete
    if cmd_args.noprompt:
        app_config.delete()
        print("The {} application configuration was removed successfully for the {} instance".format(config_name, instance.id))
    else:
        response = input("Do you want to remove the application configuration {} from the {} instance? Enter 'y' to continue or 'n' to cancel: ".format(config_name, instance.id))
        if response == "y":
            app_config.delete()
            print("The {} application configuration was removed successfully for the {} instance".format(config_name, instance.id))

    return (rc, None)

# mk-appconfig
def _mkappconfig_parser(subparsers):
    appconfig_mk = subparsers.add_parser('mkappconfig', help='Creates a configuration that enables connection to an external application')
    appconfig_mk.add_argument('config_name', help='Name of the app config', metavar='config-name')
    appconfig_mk.add_argument('--property', action='append', help='Specifies a property name and value pair to add to or change in the configuration', metavar='name=value')
    appconfig_mk.add_argument('--propfile', help='Specifies the path to a file that contains a list of application configuration properties for connecting to an external application', metavar='property-file')
    appconfig_mk.add_argument('--description', help='Specifies a description for the application configuration', metavar='description')
    _user_arg(appconfig_mk)

def _mkappconfig(instance, cmd_args, rc):
    """create an appconfig"""
    config_name, config_props, config_description = _get_config_details(cmd_args, mk=True)
    appconfig = None

    # Check if config exists by that name, if so don't do anything
    if instance.get_application_configurations(config_name):
        raise Exception("The {} application configuration already exists in the following {} instance".format(config_name, instance.id))
    else:
        # No appconfig exists by that name, create new one
        appconfig =  instance.create_application_configuration(name=config_name, properties=config_props, description=config_description)
        if appconfig:
            print("The {} application configuration was created successfully for the {} instance".format(config_name, instance.id))
        else:
            # Failed to create appconfig
            rc = 1

    return (rc, appconfig)

# ch-appconfig
def _chappconfig_parser(subparsers):
    appconfig_ch = subparsers.add_parser('chappconfig', help='Change the configuration properties that are used to make a connection to an external application')
    appconfig_ch.add_argument('config_name', help='Name of the app config', metavar='config-name')
    appconfig_ch.add_argument('--property', action='append', help='Specifies a property name and value pair to add to or change in the configuration', metavar='name=value')
    appconfig_ch.add_argument('--description', help='Specifies a description for the application configuration', metavar='description')
    _user_arg(appconfig_ch)

def _chappconfig(instance, cmd_args, rc):
    """change an appconfig"""
    config_name, config_props, config_description = _get_config_details(cmd_args, mk=False)
    new_app_config = None
    # Check if config exists by that name, if so update it
    if instance.get_application_configurations(config_name):
        appconfig = instance.get_application_configurations(config_name)[0]
        new_app_config = appconfig.update(properties=config_props, description=config_description)
        if new_app_config:
            print("The {} application configuration was updated successfully for the {} instance".format(config_name, instance.id))
        else:
            rc = 1
    else:
        # No appconfig exists by that name
        raise NameError("The {} application configuration does not exist in the {} instance".format(config_name, instance.id))

    return (rc, new_app_config)


def _get_config_details(cmd_args, mk):
    """A helper function for mkappconfig and chappconfig that gets the name, properties and description of an appconfig
    Arguments:
        cmd_args {List} -- The args passed in the command line
        mk {Boolean} -- Describes whether or not to check for a propFile arg (only mkappconfig can have a --propfile arg)

    Returns:
        config_name {String} -- The name of the appconfig
        config_props {Dictionary} -- key-value pair properties of the appconfig
        config_description {String} -- description of the appconfig
    """
    config_name = cmd_args.config_name
    config_props = {}
    config_description = None
    # If mkappconfig command (bc only this command allows for a propFile) Get props_file if given
    if mk:
        if cmd_args.propfile:
            my_file = open(cmd_args.propfile)
            prop_list = [line.rstrip() for line in my_file if not line.isspace()]
            my_file.close()
            config_props = _create_appconfig_props(config_props, prop_list)

    # Get props from command line
    # Name's specified via command line (ie --property option) override names specified in the prop file
    if cmd_args.property:
        prop_list = cmd_args.property
        config_props = _create_appconfig_props(config_props, prop_list)

    # Create the config description
    if cmd_args.description:
        config_description = cmd_args.description

    return config_name, config_props, config_description

def _create_appconfig_props(config_props, prop_list):
    """A helper function for mk-appconfig and ch-appconfig that creates the config properties dictionary

    Arguments:
        config_props {Dictionary} -- A dictionary with key-value pairs that gives the properties in an appconfig
        prop_list {List} -- A list with items of the form <Key>=<Value> that describe a given property in an appconfig

    Returns:
        config_props {Dictionary} -- A dictionary with key-value pairs that gives the properties in an appconfig
    """
    # Iterate through list of properties, check if correct format, convert each to name/value pairs add to config_props dict
    # Ex. good proplist is ['name1=value1', 'name2=value2']
    # Ex. bad proplist is ['name1=valu=e1', 'name2=value2']
    for prop in prop_list:
        name_value_pair = prop.split("=")
        if (len(name_value_pair) != 2):
            raise ValueError("The format of the following property specification is not valid: {}. The correct syntax is: <name>=<value>".format(prop))
        config_props[name_value_pair[0]] = name_value_pair[1]
    return config_props

# get-appconfig
def _getappconfig_parser(subparsers):
    appconfig_get = subparsers.add_parser('getappconfig', help='Displays the properties of a configuration that enables connection to an external application')
    appconfig_get.add_argument('config_name', help='Name of the app config', metavar='config-name')
    _user_arg(appconfig_get)

def _getappconfig(instance, cmd_args, rc):
    """get an appconfig"""
    config_name = cmd_args.config_name
    configs = instance.get_application_configurations(name = config_name)
    # Check if any configs by that name
    if (not configs):
        raise NameError("No application configuration by the name {}".format(config_name))
    config = configs[0]
    config_props = config.properties
    if not config_props:
        raise Exception("The {} application configuration has no properties defined".format(config_name))

    for key, value in config_props.items():
        try:
            json_value = json.loads(value)
            json_value = json.dumps(json_value, indent=2)
        except ValueError:
            json_value = value
        print(key + "=" + json_value)
    return (rc, config_props)

###########################################
# rmtoolkit
###########################################
def _rmtoolkit_parser(subparsers):
    toolkit_rm = subparsers.add_parser('rmtoolkit', help='Remove toolkits from a build service')
    g1 = toolkit_rm.add_argument_group(title='Toolkit selection', description='Selects which toolkits will be removed.')
    group = g1.add_mutually_exclusive_group(required=True)
    group.add_argument('--id', '-i', help='Specifies the id of the toolkit to remove', metavar='toolkit-id')
    group.add_argument('--name', '-n', help='Remove all toolkits with this name', metavar='toolkit-name')
    group.add_argument('--regex', '-r', help='Remove all toolkits where the name matches the given regex pattern', metavar='toolkit-regex')
    _user_arg(toolkit_rm)

def _rmtoolkit(instance, cmd_args, rc):
    # Get all toolkits from the build_server
    build_server = BuildService.of_endpoint(verify=False if cmd_args.disable_ssl_verify else None)

    tk_to_delete = []
    return_message = None

    # Find the toolkit matching toolkitid
    if cmd_args.id:
        try:
            matching_toolkit = build_server.get_toolkit(cmd_args.id)
            tk_to_delete.append(matching_toolkit)
        except ValueError:
            pass

    # Find all toolkits with toolkitname
    elif cmd_args.name:
        remote_toolkits = build_server.get_toolkits()
        matching_toolkits = [x for x in remote_toolkits if x.name == cmd_args.name]
        if matching_toolkits:
            tk_to_delete.extend(matching_toolkits)

    # Find all toolkits where the name matches toolkitregex
    elif cmd_args.regex:
        matching_toolkits = build_server.get_toolkits(name=cmd_args.regex)
        if matching_toolkits:
            tk_to_delete.extend(matching_toolkits)

    # If there are any toolkits to delete, delete them
    for tk in tk_to_delete:
        val = tk.delete()
        if not val:
            # If tk fails to delete, set error code and message
            rc = 1
            return_message = '1 or more toolkits failed to delete'

    return (rc, return_message)

###########################################
# lstoolkit
###########################################
def _lstoolkit_parser(subparsers):
    toolkit_ls = subparsers.add_parser('lstoolkit', help='List toolkits on the build service')
    g1 = toolkit_ls.add_argument_group(title='Toolkit selection', description='Selects which toolkits will be listed.')
    group = g1.add_mutually_exclusive_group(required=True)
    group.add_argument('--all', '-a', action='store_true', help='List all toolkits')
    group.add_argument('--id', '-i', help='Specifies the id of the toolkit to list', metavar='toolkit-id')
    group.add_argument('--name', '-n', help='List all toolkits with this name', metavar='toolkit-name')
    group.add_argument('--regex', '-r', help='List all toolkits where the name matches the given regex pattern', metavar='toolkit-regex')
    _user_arg(toolkit_ls)

def _lstoolkit(instance, cmd_args, rc):
    # Get all toolkits from the build_service
    build_server = BuildService.of_endpoint(verify=False if cmd_args.disable_ssl_verify else None)

    return_message = None

    # Find the toolkit matching toolkitid
    if cmd_args.id:
        try:
            matching_toolkit = build_server.get_toolkit(cmd_args.id)
            tk_to_list = [matching_toolkit]
        except ValueError:
            tk_to_list = []

    # Find all toolkits with toolkitname
    elif cmd_args.name:
        remote_toolkits = build_server.get_toolkits()
        tk_to_list = [x for x in remote_toolkits if x.name == cmd_args.name]

    # Find all toolkits where the name matches toolkitregex
    elif cmd_args.regex:
        tk_to_list = build_server.get_toolkits(name=cmd_args.regex)
    elif cmd_args.all:
        tk_to_list = build_server.get_toolkits()

    tk_to_list.sort(key=lambda tk : (tk.name, tk.version))

    headers = ["Name", "Version", 'RequiredProductVersion']

    # pre process the data so output is formatted nicely
    h_length = [len(x) for x in headers] # header_length
    for tk in tk_to_list:
        h_length[0] = max(len(tk.name), h_length[0])
        h_length[1] = max(len(tk.version), h_length[1])
        h_length[2] = max(len(tk.requiredProductVersion), h_length[2])

    fmt = '{:<{name_w}}  {:<{ver_w}}  {:<{pv_w}}'

    print(fmt.format(headers[0], headers[1], headers[2],
        name_w=h_length[0], ver_w=h_length[1], pv_w=h_length[2]))
    for tk in tk_to_list:
         print(fmt.format(tk.name, tk.version, tk.requiredProductVersion,
             name_w=h_length[0], ver_w=h_length[1], pv_w=h_length[2]))

    return (rc, return_message)

###########################################
# uploadtoolkit
###########################################
def _uploadtoolkit_parser(subparsers):
    toolkit_up = subparsers.add_parser('uploadtoolkit', help='Upload a toolkit to a build service')
    toolkit_up.add_argument('--path', '-p', help='Path to the toolkit to be uploaded', required=True)
    _user_arg(toolkit_up)

def _uploadtoolkit(instance, cmd_args, rc):
    bs = BuildService.of_endpoint(verify=False if cmd_args.disable_ssl_verify else None)

    return_message = None

    tk = bs.upload_toolkit(cmd_args.path)
    if tk is not None:
        print('Toolkit with id {} uploaded from {}'.format(tk.id, cmd_args.path))
    else:
        return_message = 'Toolkit not uploaded from {}'.format(cmd_args.path)
        print(return_message, file=sys.stderr )
        rc = 1

    return (rc, return_message)

###########################################
# updateoperators
###########################################
def _updateops_parser(subparsers):
    update_ops = subparsers.add_parser('updateoperators', help='Adjust a job configuration while the job is running')
    g1 = update_ops.add_argument_group(title='Job selection', description='One of these options must be chosen.')
    group = g1.add_mutually_exclusive_group(required=True)
    group.add_argument('jobid', help='Specifies a job ID.', nargs='?', metavar='jobid')
    group.add_argument('--jobname', help='Specifies the name of the job.', metavar='job-name')
    update_ops.add_argument('--jobConfig', '-g', help='Specifies the name of an external file that defines a job configuration overlay', metavar='file-name')
    update_ops.add_argument('--parallelRegionWidth', help='Specifies a parallel region name and its width', metavar='parallelRegionName=width')
    update_ops.add_argument('--force', action='store_true', help='Specifies whether to automatically stop the PEs that need to be stopped', default=False)

    _user_arg(update_ops)

def _updateops(instance, cmd_args, rc):
    return_message = None
    job_config_json = None
    job = None

    # Check that job w/ jobID or jobname exists
    if cmd_args.jobid:
        job = instance.get_job(id=str(cmd_args.jobid))
    elif cmd_args.jobname:
        jobs = instance.get_jobs(name=str(cmd_args.jobname))
        if jobs:
            job = jobs[0]

    # job doesn't exist, throw error
    if not job:
        return (1, "The job was not found")

    if cmd_args.jobConfig:
        with open(cmd_args.jobConfig) as fd:
            job_config_json = json.load(fd)
        # If empty JCO passed in, throw error
        if not job_config_json:
            return (1, 'JCO is empty')
    elif cmd_args.parallelRegionWidth:
        # If no JCO, but parallelRegionWidth arg passed in, create empty JCO and later populate w/ parallelRegionWidth
        job_config_json = {}
    else:
        # No JCO or parallelRegionWidth arg, throw error
        return (1, 'A JCO or parallelRegionWidth is required')


    if cmd_args.parallelRegionWidth:
        # Overrides the targetParallelRegion if already present in the JCO
        # else, populate empty JCO w/ parallelRegionWidth
        arr = cmd_args.parallelRegionWidth.split('=')
        if len(arr) != 2:
            raise ValueError("The format of the following submission-time parameter is not valid: {}. The correct syntax is: <name>=<value>".format(arr))
        name, width = arr[0], arr[1]

        entry = {'targetParallelRegion': {'regionName': name, 'newWidth': int(width)}}

        # If JCO is empty, and parallelRegionWidth arg is present, create JCO with arg
        if not job_config_json:
            job_config_json = {
                'jobConfigOverlays' : [
                    {'configInstructions' : {'adjustmentSection': [entry]}
                    }
                ]
            }
        else:
            # Non-empty JCO, and parallelRegionWidth arg is present, override existing in JCO

            # jobConfigOverlays is an array, where only the first jobConfigOverlay is supported
            JCO = job_config_json['jobConfigOverlays'][0]
            # Check if configInstructions already exists
            if 'configInstructions' in JCO:
                cfg_inst = JCO['configInstructions']
                # Overwrite adjustmentSection, since only 1 parallelRegion can be specified
                cfg_inst['adjustmentSection'] = [entry]
            else:
                JCO['configInstructions'] = {'adjustmentSection': [entry]}

    # If --force present, force PE to stop
    if cmd_args.force:
        JCO = job_config_json['jobConfigOverlays'][0]
        JCO['operationConfig'] = {"forcePeStopped": True}

    job_config = streamsx.topology.context.JobConfig.from_overlays(job_config_json)
    json_result = job.update_operators(job_config)

    # --- 1/13/20 JSON result is incorrect until 1Q20 fix ---
    # if json_result:
    #     file_name = str(job.name) + '_' + str(job.id) + '_config.json'
    #     with open(file_name, 'w') as outfile:
    #         json.dump(json_result, outfile)

    if json_result !=0:
        return (1, 'Update operators failed')
    else:
        print('Update operators was started on the {} instance.'.format(instance.id))
        # print('The operator configuration results were written to the following file: {}'.format(file_name))

    return (rc, return_message)

def run_cmd(args=None):
    cmd_args = _parse_args(args)

    if cmd_args.disable_ssl_verify:
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    instance = Instance.of_endpoint(
        username=cmd_args.User,
        verify=False if cmd_args.disable_ssl_verify else None)

    switch = {
    "submitjob": _submitjob,
    "canceljob": _canceljob,
    "lsjobs": _lsjobs,
    "lsappconfig": _lsappconfig,
    "rmappconfig":_rmappconfig,
    "mkappconfig": _mkappconfig,
    "chappconfig": _chappconfig,
    "getappconfig": _getappconfig,
    "rmtoolkit": _rmtoolkit,
    "lstoolkit": _lstoolkit,
    "uploadtoolkit": _uploadtoolkit,
    "updateoperators": _updateops,
    }

    extra_info = None
    rc = 0
    try:
        rc, extra_info = switch[cmd_args.subcmd](instance, cmd_args, rc)
    except Exception as e:
        rc = 1
        print(e, file=sys.stderr)
    return (rc, extra_info)

def main(args=None):
    """ Mimic streamtool using the REST api for ICP4D.
    """
    streamsx._streams._version._mismatch_check('streamsx.topology.context')

    rc, extra_info = run_cmd(args)
    return rc

def _parse_args(args):
    """ Argument parsing
    """
    cmd_parser = argparse.ArgumentParser(description='IBM Streams command line interface using REST.')
    cmd_parser.add_argument('--disable-ssl-verify', action='store_true', help='Disable SSL verification.')

    subparsers = cmd_parser.add_subparsers(help='Supported commands', dest='subcmd')

    _submitjob_parser(subparsers)
    _canceljob_parser(subparsers)
    _lsjobs_parser(subparsers)
    _lsappconfig_parser(subparsers)
    _rmappconfig_parser(subparsers)
    _mkappconfig_parser(subparsers)
    _chappconfig_parser(subparsers)
    _getappconfig_parser(subparsers)
    _rmtoolkit_parser(subparsers)
    _lstoolkit_parser(subparsers)
    _uploadtoolkit_parser(subparsers)
    _updateops_parser(subparsers)

    cmd_args = cmd_parser.parse_args(args)
    if cmd_args.subcmd is None:
        cmd_parser.print_help()
        exit(0)
    else:
        return cmd_args

def _user_arg(parser):
    parser.add_argument('--User', '-U', help='Specifies an IBM Streams user ID that has authority to run the command.', metavar='user')


if __name__ == '__main__':
    exit_code = main()
    sys.exit(exit_code)

