#!/usr/bin/env python
# pyinfra
# File: bin/pyinfra
# Desc: __main__ for pyinfra

'''
pyinfra
Docs: pyinfra.readthedocs.org/en/develop

Usage:
    pyinfra -i INVENTORY DEPLOY [-v -vv options]
    pyinfra -i INVENTORY --run OP ARGS [-v -vv options]
    pyinfra -i INVENTORY --fact FACT [-v options]
    pyinfra (--facts | --help | --version)

Deploy options:
    DEPLOY               Deploy script filename.
    -i INVENTORY         Inventory script filename or single hostname.
    --run OP ARGS        Run a single operation with args.
    --fact FACT          Name of fact to run/test.
    --limit HOSTNAME     Limit the inventory at runtime, supports *wildcards.
    --serial             Run commands on one host at a time.
    --nowait             Don't wait for all hosts at each operation.
    -v -vv               Prints remote input/output in realtime. -vv prints facts output.
    --dry                Only print proposed changes.
    --debug              Print debug info.

Config options:
    -p --port PORT       SSH port number.
    -u --user USER       SSH user.
    --key KEY_FILE       SSH private key.
    --key-password PASS  SSH key password.
    --sudo               Use sudo.
    --sudo-user USER     Which user to sudo to.
    --password PASS      SSH password auth (bad).
'''

from __future__ import division

from gevent import monkey
monkey.patch_all()

import sys
import signal
import logging
import traceback
from os import getcwd, path

from docopt import docopt
from termcolor import colored

# Colorama patch to enable termcolor on Windows
from colorama import init as colorama_init
colorama_init()

from pyinfra import logger, pseudo_state, pseudo_host, hook
from pyinfra.local import set_print_local
from pyinfra.cli import (
    FakeHost,
    run_hook, dump_state, setup_logging,
    make_inventory, load_config, load_deploy_config, setup_arguments,
    print_meta, print_results, print_fact, print_facts_list
)

from pyinfra.api import State
from pyinfra.api.ssh import connect_all
from pyinfra.api.operation import add_op
from pyinfra.api.operations import run_ops
from pyinfra.api.attrs import FallbackAttrData
from pyinfra.api.facts import get_facts, set_print_facts
from pyinfra.api.exceptions import PyinfraException


# Handle ctrl+c
def _signal_handler(signum, frame):
    print 'Exiting upon user request!'
    sys.exit(0)

signal.signal(signal.SIGINT, _signal_handler)

# Exit handler
def _exit(code=0):
    print
    print '<-- Thank you, goodbye'
    print

    sys.exit(code)


print
print '### {0}'.format(colored('Welcome to pyinfra', attrs=['bold']))
print


# Get arguments
arguments = docopt(__doc__, version='pyinfra')

# Setup logging
log_level = logging.DEBUG if arguments['--debug'] else logging.INFO
setup_logging(log_level)


try:
    # Setup arguments
    arguments = setup_arguments(arguments)

    # Setup printing
    print_output = arguments['verbose'] > 0
    if arguments['deploy'] is None and arguments['op'] is None:
        print_facts = print_output
    else:
        print_facts = arguments['verbose'] > 1

    set_print_facts(print_facts, print_output)
    set_print_local(print_output)

    # Quickly list facts & exit if desired
    if arguments['list_facts']:
        logger.info('Available facts list:')
        print_facts_list()
        _exit()

    deploy_dir = getcwd()

    # This is the most common case: we have a deploy file so use it's pathname
    if arguments['deploy'] is not None:
        deploy_dir = path.dirname(arguments['deploy'])

    # If we have a valid inventory, look in it's path and it's parent for group_data or
    # config.py to indicate deploy_dir (--fact, --run)
    elif arguments['inventory'] and path.isfile(arguments['inventory']):
        inventory_dir, _ = path.split(arguments['inventory'])
        above_inventory_dir, _ = path.split(inventory_dir)

        for inventory_path in (inventory_dir, above_inventory_dir):
            if any((
                path.isdir(path.join(inventory_path, 'group_data')),
                path.isfile(path.join(inventory_path, 'config.py'))
            )):
                deploy_dir = inventory_path

    # Load up the inventory from the filesystem
    inventory, inventory_group = make_inventory(
        arguments['inventory'],
        deploy_dir=deploy_dir,
        limit=arguments['limit'],
        ssh_user=arguments['user'],
        ssh_key=arguments['key'],
        ssh_key_password=arguments['key_password'],
        ssh_port=arguments['port'],
        ssh_password=arguments['password']
    )

    # Load up any config.py from the filesystem
    config = load_config(deploy_dir)
    # Arg based overrides
    if arguments['sudo']:
        config.SUDO = True
        if arguments['sudo_user']:
            config.SUDO_USER = arguments['sudo_user']

    # Create/set the state
    state = State(inventory, config)
    state.deploy_dir = deploy_dir

    # Attach to pseudo state
    pseudo_state.set(state)

    # Load any hooks/config from the deploy file
    state.active = False
    pseudo_host.set(FakeHost())
    load_deploy_config(arguments['deploy'], config)
    state.active = True

    # Setup the data to be passed to config hooks
    hook_data = FallbackAttrData(
        state.inventory.get_override_data(),
        state.inventory.get_group_data(inventory_group),
        state.inventory.get_data()
    )

    # Run the before_connect hook if provided
    run_hook(state, 'before_connect', hook_data)

    # Connect to all the servers
    print '--> Connecting to hosts...'
    connect_all(state)

    # Check we've connected to something
    n_connected_hosts = len(state.inventory.connected_hosts)
    if n_connected_hosts == 0:
        raise PyinfraException('No hosts connected, exiting')

    # Check we've not failed
    if state.config.FAIL_PERCENT is not None:
        percent_failed = (1 - n_connected_hosts / len(state.inventory)) * 100
        if percent_failed >= state.config.FAIL_PERCENT:
            raise PyinfraException('Over {0}% of hosts failed, exiting'.format(
                state.config.FAIL_PERCENT
            ))

    print

    # Run the before_connect hook if provided
    run_hook(state, 'before_facts', hook_data)

    # Just getting a fact?
    if arguments['fact']:
        if ':' in arguments['fact']:
            fact, fact_args = arguments['fact'].split(':')
            fact_args = fact_args.split(',')
        else:
            fact = arguments['fact']
            fact_args = None

        fact_data = get_facts(
            state, fact, args=fact_args,
            sudo=arguments['sudo'], sudo_user=arguments['sudo_user'],
            print_output=print_output
        )
        print_fact(fact_data)
        _exit()

    # We're building a deploy!
    print '--> Building deploy scripts...'

    # Deploy file
    if arguments['deploy']:
        # This actually does the op build
        for host in inventory:
            pseudo_host.set(host)
            execfile(arguments['deploy'])
            logger.info('{0} {1}'.format(
                '[{}]'.format(colored(host.ssh_hostname, attrs=['bold'])),
                colored('Ready', 'green')
            ))

    # One off op run
    else:
        # Setup args if present
        args, kwargs = [], {}
        if isinstance(arguments['op_args'], tuple):
            args, kwargs = arguments['op_args']

        # Add the op w/args
        add_op(
            state, arguments['op'],
            *args, **kwargs
        )

    # Always show meta output
    print
    print '--> Proposed changes:'
    print_meta(state)

    # If debug, dump state (ops, op order, op meta) now
    if arguments['debug']:
        dump_state(state)

    # Run the operations we generated with the deploy file
    if not arguments['dry']:
        print

        # Run the before_deploy hook if provided
        run_hook(state, 'before_deploy', hook_data)

        print '--> Beginning operation run...'
        run_ops(
            state,
            serial=arguments['serial'],
            nowait=arguments['nowait'],
            print_output=print_output,
            print_lines=True
        )

        # Run the after_deploy hook if provided
        run_hook(state, 'after_deploy', hook_data)

        print '--> Results:'
        print_results(state)

except hook.Error as e:
    print
    sys.stderr.write(
        '--> {0}: '.format(colored('hook exception', 'red', attrs=['bold']))
    )
    logger.warning(e)
    _exit(1)

# Capture/log internal exceptions
except PyinfraException as e:
    print
    sys.stderr.write(
        '--> {0}: '.format(colored('pyinfra exception', 'red', attrs=['bold']))
    )
    logger.warning(e)
    _exit(1)

# Capture/dump unexpected exceptions
except Exception as e:
    print
    sys.stderr.write(
        '--> {0}: '.format(colored('unknown exception', 'red', attrs=['bold']))
    )

    # Dev mode, so lets dump as much data as we have
    error_type, value, trace = sys.exc_info()
    print '----------------------'
    traceback.print_tb(trace)
    logger.critical('{0}: {1}'.format(error_type.__name__, value))
    print '----------------------'
    _exit(1)


_exit()
