#!/usr/bin/env python
""" Launches or updates cloudformation stacks based on the given config.

The script is smart enough to figure out if anything (the template, or
parameters) has changed for a given stack. If not, it will skip that stack.

Can also pull parameters from other stack's outputs.
"""

import argparse
from collections import Mapping
import copy
import logging
import yaml

from stacker.builder import Builder
from stacker.util import handle_hooks
from stacker.config import parse_config

logger = logging.getLogger()

DEBUG_FORMAT = ('[%(asctime)s] %(levelname)s %(name)s:%(lineno)d'
                '(%(funcName)s) - %(message)s')
INFO_FORMAT = ('[%(asctime)s] %(message)s')

ISO_8601 = '%Y-%m-%dT%H:%M:%S'


def get_stack_definitions(config, stack_list):
    """ Extract stack definitions from the config.

    If no stack_list given, return stack config as is.
    """
    if not stack_list:
        return config['stacks']
    return [s for s in config['stacks'] if s['name'] in stack_list]


class KeyValueAction(argparse.Action):
    def __init__(self, option_strings, dest, default=None, nargs=None,
                 **kwargs):
        if nargs:
            raise ValueError("nargs not allowed")
        default = default or {}
        super(KeyValueAction, self).__init__(option_strings, dest, nargs,
                                             default=default, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        if not isinstance(values, Mapping):
            raise ValueError("type must be 'key_value'")
        if not getattr(namespace, self.dest):
            setattr(namespace, self.dest, {})
        getattr(namespace, self.dest).update(values)


def key_value_arg(string):
    try:
        k, v = string.split("=", 1)
    except ValueError:
        raise argparse.ArgumentTypeError(
            "%s does not match KEY=VALUE format." % string)
    return {k: v}


def yaml_file_type(yaml_file):
    """ Reads a yaml file and returns the resulting data. """
    with open(yaml_file) as fd:
        return yaml.load(fd)


def parse_args():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('-r', '--region', default='us-east-1',
                        help="The AWS region to launch in. Default: "
                             "%(default)s")
    parser.add_argument('-e', '--environment', type=yaml_file_type,
                        default={},
                        help="Path to a yaml environment file. The values in "
                             "the environment file can be used in the stack "
                             "config as if it were a string.Template type: "
                             "https://docs.python.org/2/library/string.html"
                             "#template-strings")
    parser.add_argument('-m', '--max-zones', type=int,
                        help="Gives you the ability to limit the # of zones "
                             "that resources will be launched in. If not "
                             "given, then resources will be launched in all "
                             "available availability zones.")
    parser.add_argument('-v', '--verbose', action='count', default=0,
                        help='Increase output verbosity. May be specified up '
                             'to twice.')
    parser.add_argument("-p", "--parameter", dest="parameters",
                        metavar="PARAMETER=VALUE", type=key_value_arg,
                        action=KeyValueAction, default={},
                        help="Adds parameters from the command line "
                             "that can be used inside any of the stacks "
                             "being built. Can be specified more than once.")
    parser.add_argument("--stacks", action="append",
                        metavar="STACKNAME", type=str,
                        help="Only work on the stacks given. Can be "
                             "specified more than once. If not specified "
                             "then stacker will work on all stacks in the "
                             "config file.")
    parser.add_argument('namespace',
                        help='The namespace for the stack collection. This '
                             'will be used as the prefix to the '
                             'cloudformation stacks as well as the s3 bucket '
                             'where templates are stored.')
    parser.add_argument('config', type=argparse.FileType(),
                        help="The config file where stack configuration is "
                             "located. Must be in yaml format.")
    return parser.parse_args()


def setup_logging(verbosity):
    log_level = logging.INFO
    log_format = INFO_FORMAT
    if verbosity > 0:
        log_level = logging.DEBUG
        log_format = DEBUG_FORMAT
    if verbosity < 2:
        logging.getLogger('boto').setLevel(logging.CRITICAL)

    return logging.basicConfig(format=log_format, datefmt=ISO_8601,
                               level=log_level)

if __name__ == '__main__':
    args = parse_args()
    setup_logging(args.verbose)
    parameters = copy.deepcopy(args.parameters)
    config_string = args.config.read()

    config = parse_config(config_string, args.environment)

    mappings = config['mappings']

    builder = Builder(args.region, args.namespace, mappings=mappings,
                      parameters=parameters)

    stack_definitions = get_stack_definitions(config, args.stacks)
    stack_names = [s['name'] for s in stack_definitions]
    handle_hooks('pre_build', config.get('pre_build', None), args.region,
                 args.namespace, mappings, parameters)
    logger.info("Working on stacks: %s", ', '.join(stack_names))
    builder.build(stack_definitions)
    handle_hooks('post_build', config.get('post_build', None), args.region,
                 args.namespace, mappings, parameters)
