#!/usr/bin/env python
# coding=utf-8
#
# Copyright © Splunk, Inc. All Rights Reserved.

from __future__ import absolute_import, division, print_function, unicode_literals

from os import path
import sys
import json

from slim.app import *
from slim.command import *
from slim.utils import *


# Argument parser definition

parser = SlimArgumentParser(
    description='split an app source package into a set of targeted deployment packages',
    epilog='The targeted deployment packages created are based on user-defined deployment specifications. A deployment '
           'specification can contain any combination of three different types of Splunk workloads: indexer (named as '
           '"_indexers"), search head (named as "_search_heads") and forwarder (named as "_forwarders").'
)

parser.add_app_package()
parser.add_argument_help()
parser.add_installation()
parser.add_output_directory(description='deployment packages')
parser.add_repository()

# Command-specific arguments

parser.add_argument(
    '-c', '--combine-search-head-indexer-workloads',
    action='store_const', const=True, default=False,
    help='combine search head and indexer workloads into a single deployment package')

parser.add_argument(
    '-d', '--deployment-packages',
    type=SlimDeploymentSpecificationArgument(), nargs='+', default=[],
    help='specify a set of deployment packages by name, workload, and--for forwarder workloads--input groups',
    metavar='<specification>')

parser.add_argument(
    '-f', '--forwarder-workloads',
    type=SlimForwarderWorkloadsArgument(), default=None,
    help='map app input groups to a set of server classes (default: ["_search_heads", "_indexers", "_forwarders"])',
    metavar='<forwarder-workloads>')

# TODO: Make this the default state for 'partition' and remove the 'install' options to split logically (?)
# What's the use case for not updating the input installation graph, if there is one? Is the output installation graph
# not always potentially useful to customers using the CLI to partition an app? Why force customers to run another
# command to achieve the same end: partitioning an app and then updating an installation graph?

parser.add_argument(
    '-p', '--partition-only',
    action='store_const', const=True, default=False,
    help='verify installation graph to ensure it is unchanged after partitioning; useful when you are partitioning an '
         'app for re-deployment')


def main(args):

    SlimLogger.step('Extracting source from ', encode_filename(args.source), '...')
    app_source = get_app_source(args.source, None)
    server_collection = get_app_server_class_collection(args.installation, args.repository)

    if not args.partition_only:

        SlimLogger.step('Adding ', app_source.qualified_id, ' to installation graph...')

        server_collection.add(app_source, get_deployment_specifications(
            args.deployment_packages, args.combine_search_head_indexer_workloads, args.forwarder_workloads
        ))
        SlimLogger.exit_on_error()

        # Save the resulting installation graph, even if there are no changes; when len(deployment_packages) == 0

        filename = path.join(args.output_dir, 'installation-update.json')
        server_collection.save(filename)

        SlimLogger.information('Saved updated installation graph to ', encode_filename(filename))

    _partition(app_source, server_collection, args.output_dir, partition_all=True)


def partition(source, installation_graph, output_dir):

    if isinstance(installation_graph, dict):
        installation_graph = json.dumps(installation_graph)

    string_io_type = SlimStringIOArgument(name="installation_graph.json")
    installation = string_io_type(value=installation_graph)

    SlimLogger.step('Extracting source from ', encode_filename(source), '...')

    app_source = get_app_source(source, None)
    server_collection = get_app_server_class_collection(installation, slim_configuration.repository_path)

    _partition(app_source, server_collection, output_dir, partition_all=False)


def _partition(app_source, server_collection, output_dir, partition_all):
    """ Partition an app into deployment packages targeting a collection of server classes.

    :param app_source: Represents the app to be partitioned.
    :type app_source: AppSource

    :param server_collection: Represents the set of server classes.
    :type server_collection: ServerClassCollections

    :param output_dir: Path to output directory
    :type output_dir: string

    :param partition_all:
    :type partition_all: bool

    """
    SlimLogger.step('Partitioning ', app_source.qualified_id, '...')
    deployment_packages = server_collection.partition(app_source, output_dir, partition_all)

    if len(deployment_packages):
        app_id = app_source.qualified_id
        SlimLogger.information('Generated deployment packages for ', app_id, ':\n  ', '\n  '.join(deployment_packages))
    else:
        SlimLogger.information(
            'No deployment packages generated for ', app_source.id, ' because there is nothing meaningful to deploy'
        )

# Wrapper functions to handle errors


def get_app_source(package, configuration):
    app_source = AppSource(package, configuration)
    SlimLogger.exit_on_error()
    return app_source


def get_app_server_class_collection(installation, repository):
    server_classes = AppServerClassCollection.load(installation, repository)
    SlimLogger.exit_on_error()
    return server_classes


def get_deployment_specifications(
        deployment_specifications, combine_search_head_indexer_workloads, forwarder_deployment_specifications
):
    deployment_specifications = AppDeploymentSpecification.get_deployment_specifications(
        deployment_specifications, combine_search_head_indexer_workloads, forwarder_deployment_specifications)
    SlimLogger.exit_on_error()
    return deployment_specifications


if __name__ == '__main__':
    # noinspection PyBroadException
    try:
        main(parser.parse_args(sys.argv[1:]))
    except SystemExit:
        raise
    except:
        SlimLogger.set_debug(True)
        SlimLogger.fatal(exception_info=sys.exc_info())
