#!/usr/bin/env python
"""
Juju Deployer

Deployment automation for juju.

"""


from __future__ import absolute_import
from __future__ import print_function
import argparse
import errno
import logging
import os
import sys
import time

from deployer.config import ConfigStack
from deployer.env import select_runtime
from deployer.action import diff, importer
from deployer.utils import (
    ErrorExit, setup_logging, get_env_name, get_juju_major_version)


def setup_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '-c', '--config',
        help=('File containing deployment(s) json config. This '
              'option can be repeated, with later files overriding '
              'values in earlier ones.'),
        dest='configs', action='append')
    parser.add_argument(
        '-d', '--debug', help='Enable debugging to stdout',
        dest="debug",
        action="store_true", default=False)
    parser.add_argument(
        '-L', '--local-mods',
        help='Allow deployment of locally-modified charms',
        dest="no_local_mods", default=True, action='store_false')
    parser.add_argument(
        '-u', '--update-charms',
        help='Update existing charm branches',
        dest="update_charms", default=False, action="store_true")
    parser.add_argument(
        '-l', '--ls', help='List available deployments',
        dest="list_deploys", action="store_true", default=False)
    parser.add_argument(
        '-D', '--destroy-applications',
        help='Destroy all applications (do not terminate machines)',
        dest="destroy_applications", action="store_true",
        default=False)
    parser.add_argument(
        '-T', '--terminate-machines',
        help=('Terminate all machines but the bootstrap node. '
              'Destroy any applications that exist on each. '
              'Use -TT to forcefully terminate.'),
        dest="terminate_machines", action="count", default=0)
    parser.add_argument(
        '-t', '--timeout',
        help='Timeout (sec) for entire deployment (45min default)',
        dest='timeout', action='store', type=int, default=2700)
    parser.add_argument(
        "-f", '--find-application', action="store", type=str,
        help='Find hostname from first unit of a specific application.',
        dest="find_application")
    parser.add_argument(
        "-b", '--branch-only', action="store_true",
        help='Update vcs branches and exit.',
        dest="branch_only")
    parser.add_argument(
        "-S", '--skip-unit-wait', action="store_true",
        help="Don't wait for units to come up, deploy, add rels and exit.")
    parser.add_argument(
        '-B', '--bootstrap',
        help=('Bootstrap specified environment, blocks until ready'),
        dest="bootstrap", action="store_true",
        default=False)
    parser.add_argument(
        '-s', '--deploy-delay', action='store', type=float,
        help=("Time in seconds to sleep between 'deploy' commands, "
              "to allow machine provider to process requests. On "
              "terminate machines this also signals waiting for "
              "machine removal."),
        dest="deploy_delay", default=0)
    parser.add_argument(
        '-e', '--environment', action='store', dest='juju_env',
        help=('Deploy to a specific Juju environment or model. '
              'For Juju 2 models, must be specified with the '
              '<controller>:<model> notation.'),
        default=os.getenv('JUJU_ENV'))
    parser.add_argument(
        '-o', '--override', action='append', type=str,
        help=('Override *all* config options of the same name '
              'across all applications.  Input as key=value.'),
        dest='overrides', default=None)
    parser.add_argument(
        '--series', type=str,
        help=('Override distro series in config files'),
        dest='series', default=None)
    parser.add_argument(
        '-v', '--verbose', action='store_true', default=False,
        dest="verbose", help='Verbose output')
    parser.add_argument(
        '-W', '--watch', help='Watch environment changes on console',
        dest="watch", action="store_true", default=False)
    parser.add_argument(
        '-r', "--retry", default=0, type=int, dest="retry_count",
        help=("Resolve CLI and unit errors via number of retries (default: 0)."
              " Either standalone or in a deployment"))
    parser.add_argument(
        '--ignore-errors', action='store_true', dest='ignore_errors',
        help='Proceed with the bundle deployment ignoring units errors. '
             'Unit errors are also automatically ignored if --retry != 0')
    parser.add_argument(
        "--diff", action="store_true", default=False,
        help=("Generate a delta between a configured deployment and a running"
              " environment."))
    parser.add_argument(
        '-w', '--relation-wait', action='store', dest='rel_wait',
        default=60, type=int,
        help=('Number of seconds to wait before checking for '
              'relation errors after all relations have been added '
              'and subordinates started. (default: 60)'))
    parser.add_argument(
        '-n', '--no-relations',
        default=False, dest='no_relations', action='store_true',
        help=('Do not add relations to environment, just applications/units '
              '(default: False)'))
    parser.add_argument("--description", help=argparse.SUPPRESS,
                        action="store_true")
    parser.add_argument("deployment", nargs="?")
    return parser


def main():
    stime = time.time()
    try:
        run()
    except ErrorExit:
        logging.getLogger('deployer.cli').info(
            "Deployment stopped. run time: %0.2f", time.time() - stime)
        sys.exit(1)


def run():
    parser = setup_parser()
    options = parser.parse_args()

    if options.description:
        print("Tool for declarative management of complex deployments.")
        sys.exit(0)

    # Debug implies watching and verbose
    if options.debug:
        options.watch = options.verbose = True
    setup_logging(options.verbose, options.debug)

    log = logging.getLogger("deployer.cli")
    start_time = time.time()

    env_name = get_env_name(options.juju_env)
    try:
        env = select_runtime(env_name, options)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise
        log.error("No juju binary found, have you installed juju?")
        sys.exit(1)
    log.debug('Using runtime %s on %s', env.__class__.__name__, env_name)

    if get_juju_major_version() == 2:
        if len(env_name.split(':')) != 2:
            log.error("Juju 2 environment names must be in "
                      "<controller>:<model> format.")
            sys.exit(1)
        if options.bootstrap:
            log.error("Bootstrap of a Juju 2 cloud is not supported.")
            sys.exit(1)

    config = ConfigStack(options.configs or [], options.series)

    # Destroy applications and exit
    if options.destroy_applications or options.terminate_machines:
        log.info("Resetting environment...")
        env.connect()
        env.reset(terminate_machines=bool(options.terminate_machines),
                  terminate_delay=options.deploy_delay,
                  watch=options.watch,
                  force_terminate=options.terminate_machines > 1)
        log.info("Environment reset in %0.2f", time.time() - start_time)
        sys.exit(0)

    # Display application info and exit
    if options.find_application:
        address = env.get_service_address(options.find_application)
        if address is None:
            log.error("Application not found %r", options.find_application)
            sys.exit(1)
        elif not address:
            log.warning("Application: %s has no address for first unit",
                        options.find_application)
        else:
            log.info(
                "Application: %s address: %s",
                options.find_application, address)
            print(address)
        sys.exit(0)

    # Just resolve/retry hooks in the environment
    if not options.deployment and options.retry_count:
        log.info("Retrying hooks for error resolution")
        env.connect()
        env.resolve_errors(
            options.retry_count, watch=options.watch, timeout=options.timeout)

    # Arg check on config files and deployment name.
    if not options.configs:
        log.error("Config files must be specified")
        sys.exit(1)

    config.load()

    # Just list the available deployments
    if options.list_deploys:
        print(("\n".join(sorted(config.keys()))))
        sys.exit(0)

    # Do something to a deployment
    if not options.deployment:
        # If there's only one option then use it.
        if len(list(config.keys())) == 1:
            options.deployment = list(config.keys())[0]
            log.info("Using deployment %s", options.deployment)
        else:
            log.error(
                "Deployment name must be specified. available: %s",
                tuple(sorted(config.keys())))
            sys.exit(1)

    deployment = config.get(options.deployment)

    if options.diff:
        diff.Diff(env, deployment, options).run()
        return

    # Import it
    log.info("Starting deployment of %s", options.deployment)
    importer.Importer(env, deployment, options).run()

    # Deploy complete
    log.info("Deployment complete in %0.2f seconds" % (
        time.time() - start_time))


if __name__ == '__main__':
    main()
