#!/usr/bin/env python
#
#   YADT - an Augmented Deployment Tool
#   Copyright (C) 2010-2014  Immobilien Scout GmbH
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
The yadtshell

Usage:
yadtshell (status|info) [options]
yadtshell (start|stop) SERVICE-URI ... [options]
yadtshell restart SERVICE-URI... [options]
yadtshell update [HOST-URI...] [-y] [--reboot | --no-reboot] [options]
yadtshell updateartefact ARTEFACT-URI ... [options]
yadtshell lock -m MESSAGE HOST-URI ... [options] [--force]
yadtshell unlock HOST-URI ... [options]
yadtshell ignore -m MESSAGE SERVICE-URI ... [options] [--force]
yadtshell unignore SERVICE-URI ... [options]
yadtshell dump [URI-PATTERN...] [--attribute --show-pending-updates --show-current-artefacts]

Options:
-n --dryrun               do not alter the system
-v --verbose              be more verbose
--tracking-id STRING      lets user define a tracking id
--no-final-status         do not fetch status of target after action

-m --message MESSAGE      reason
-p --parallel PSPEC       how to execute actions in parallel [default: 1]
-y --forcedyes            say yes to all questions
--force                   force execution
--reboot                  reboot servers if needed during an update
--no-reboot               do not reboot servers during an update, even if needed
--session-id SESSIONID    optional ID for session handling
"""

import logging
import sys

from twisted.internet import reactor
from twisted.python import log
from twisted.internet.task import deferLater
from yadtshell.commandline import (EXIT_CODE_CANCELED_BY_USER,
                                   determine_command_from_arguments,
                                   infer_options_from_arguments)
import yadtshell.helper

from yadtshell.settings import SettingsError

from docopt import docopt
arguments = docopt(__doc__)

reactor.return_code = 127

cmd = determine_command_from_arguments(arguments)
uris = (arguments['ARTEFACT-URI'] + arguments['HOST-URI'] +
        arguments['SERVICE-URI'] + arguments['URI-PATTERN'])
opts = infer_options_from_arguments(arguments)

warning_after_error = None

if opts.get('session_id'):
    yadtshell.helper.SESSION_ID = opts['session_id']

observer = log.PythonLoggingObserver()
observer.start()

import yadtshell
logger = logging.getLogger('yadtshell')

try:
    yadtshell.settings.load_settings_and_create_dirs(log_to_file=(cmd not in ['dump', 'info']))
except SettingsError, e:
    logger.critical(e)
    sys.exit(1)

if opts.get('tracking_id'):
    yadtshell.settings.tracking_id = opts.get('tracking_id')

logging.getLogger("twisted").setLevel(logging.WARN)
logging.getLogger("broadcaster").setLevel(logging.WARN)

if opts.get('verbose'):
    yadtshell.settings.console_stdout_handler.setLevel(logging.DEBUG)

if opts.get('dryrun'):
    yadtshell.settings.ybc = yadtshell.settings.DummyBroadcaster()

yadtshell.settings.reboot_enabled = opts.get('reboot')

if uris:
    try:
        components = yadtshell.util.restore_current_state()
    except IOError, e:
        logger.debug("an exception occured during restore_current_state: %s" % str(e))
        logger.debug("no status found, calling 'yadtshell status' implicitly")
        import subprocess
        # TODO avoid subprocess?!
        subprocess.call(["yadtshell", "status"])
        components = yadtshell.util.restore_current_state()

    uris = yadtshell.helper.expand_hosts(uris)
    uris = yadtshell.helper.glob_hosts(components, uris)

    def is_a_valid_host_uri_or_other_uri(uri):
        if uri.startswith('host://'):
            component = components[uri]
            return component.is_reachable()
        if uri.startswith('artefact://'):
            return True
        if isinstance(components[uri], yadtshell.components.ReadonlyService):
            logger.warning("Skipping %s because it is read-only" % uri)
            return False
        return uri in components

    uris = filter(is_a_valid_host_uri_or_other_uri, uris)

    if not uris:
        logger.error(
            'Could not resolve URIs. Check for typos or syntax issues.')
        sys.exit(1)


def create_simple_plan(cmd, uris):
    action_set = set()
    for component_name in uris:
        component = components.get(component_name, None)
        if not component:
            component = components[
                yadtshell.uri.change_version(component_name, 'current')]
        if not component:
            logger.warning('could not resolve uri %s' % component_name)
            continue
        action_set.add(
            yadtshell.actions.Action(cmd, component.uri, kwargs=opts))
    plan = yadtshell.actions.ActionPlan(cmd, action_set)
    yadtshell.util.dump_action_plan(cmd, plan)
    return plan


def createDeferredFromPlan(plan):
    plan = yadtshell.metalogic.apply_instructions(plan, opts.get('parallel'))
    yadtshell.util.dump_action_plan(cmd, plan)
    am = yadtshell.ActionManager()
    return am.action(flavor=cmd, **opts)


deferred = None

if cmd == 'status':
    deferred = yadtshell.status(hosts=uris, **opts)
elif cmd == 'info':
    yadtshell.info(**opts)
    sys.exit(0)
elif cmd == 'dump':
    yadtshell.dump(uris, **opts)
    sys.exit(0)
elif cmd == 'update':
    deferred = yadtshell.status()
    deferred.addCallback(
        yadtshell.update.compare_versions,
        uris,
        **opts
    )
    am = yadtshell.ActionManager()
    deferred.addCallback(am.action, **opts)
elif cmd in ['ignore', 'unignore', 'lock', 'unlock', 'updateartefact']:
    plan = create_simple_plan(cmd, uris)
    deferred = createDeferredFromPlan(plan)
elif cmd == 'restart':
    warning_after_error = "Do _not_ simply retry this command; for further details, see https://github.com/yadt/yadtshell/wiki/Command-Restart"
    deferred = yadtshell.status()
    deferred.addCallback(
        yadtshell.restart,
        uris,
        **opts
    )
    am = yadtshell.ActionManager()
    deferred.addCallback(am.action, **opts)
else:
    try:
        plan = yadtshell.metalogic.metalogic(cmd, uris)
        deferred = createDeferredFromPlan(plan)
    except Exception, e:
        logger.critical('an error occured while trying to "%s %s"' %
                        (cmd, ', '.join(uris)))
        logger.debug(e)
        # TODO event failed needed here
        sys.exit(1)

commands_that_change_state = ['stop', 'start', 'restart',
                              'update', 'updateartefact',
                              'ignore', 'lock', 'unignore', 'unlock']
if cmd in commands_that_change_state and not opts.get('no_final_status'):
    deferred.addCallback(yadtshell.status)


def publish_result():
    if reactor.return_code == 0:
        state = 'finished'
    else:
        state = 'failed'
        import socket
        yadtshell.settings.ybc._sendEvent(
            'error-info', data=None,
            tracking_id=yadtshell.settings.tracking_id,
            target=yadtshell.settings.ybc.target,
            host=socket.gethostname(),
            log_file=yadtshell.settings.log_file)
    yadtshell.settings.ybc.publish_cmd(
        cmd=cmd, state=state, tracking_id=yadtshell.settings.tracking_id)

    logger.debug('waiting for outstanding events to be delivered')
    return deferLater(reactor, 2, lambda: None)


def publish_start():
    yadtshell.settings.ybc.publish_cmd(
        cmd=cmd, state='started', tracking_id=yadtshell.settings.tracking_id)


try:
    deferred.addErrback(yadtshell.twisted.report_error, logger.debug)
    deferred.addBoth(yadtshell.twisted.stop_and_return)

    if not reactor.running:
        yadtshell.settings.ybc.addOnSessionOpenHandler(publish_start)
        reactor.addSystemEventTrigger('before', 'shutdown', publish_result)
        reactor.run()

except yadtshell.actions.ActionException, e:
    reactor.return_code = e.exitcode
    msg = e.message
    if e.rootcause:
        msg += ': ' + str(e.rootcause)
    logger.critical(msg)
except BaseException as e:
    reactor.return_code = 2
    logger.critical(str(e))

if reactor.return_code == 0:
    print yadtshell.settings.term.render('${GREEN}${BOLD}%s SUCCESSFUL${NORMAL}' % cmd.upper())

elif reactor.return_code == EXIT_CODE_CANCELED_BY_USER:
    print yadtshell.settings.term.render('${BG_YELLOW}${BOLD}%s CANCELLED BY USER${NORMAL}' % cmd.upper())
    if warning_after_error:
        logger.warn(warning_after_error)

else:
    print yadtshell.settings.term.render('${RED}${BOLD}%s FAILED${NORMAL}' % cmd.upper())
    if warning_after_error:
        logger.warn(warning_after_error)
    logger.critical('exit code: %i' % reactor.return_code)
    logger.info('For details see: "{0}"'.format(yadtshell.settings.log_file))
sys.exit(reactor.return_code)
