from batou import output, DeploymentError, ConfigurationError
from batou import remote_core
from batou.update import generate_bootstrap
import execnet.gateway_io
import os
import yaml
import subprocess
import sys

# Monkeypatch execnet to support 'vagrant ssh' and 'kitchen exec'.
# 'vagrant' support has been added to 'execnet' release 1.4.


def get_kitchen_ssh_connection_info(name):
    cmd = 'kitchen', 'diagnose', '--log-level=error', name
    info = yaml.load(subprocess.check_output(cmd))
    (instance,) = info['instances'].values()
    state = instance['state_file']
    return [
        '-o', 'StrictHostKeyChecking=no',
        '-i', state['ssh_key'],
        '-p', state['port'],
        '-l', state['username'],
        state['hostname'],
    ]


def new_ssh_args(spec):
    from execnet.gateway_io import popen_bootstrapline
    remotepython = spec.python or 'python'
    if spec.type == 'vagrant':
        args = ['vagrant', 'ssh', spec.ssh, '--', '-C']
    elif spec.type == 'kitchen':
        # TODO: this should really use:
        #   args = ['kitchen', 'exec', spec.ssh, '-c']
        # but `exec` apparently doesn't connect stdin (yet)...
        args = ['ssh', '-C'] + get_kitchen_ssh_connection_info(spec.ssh)
    else:
        args = ['ssh', '-C']
    if spec.ssh_config is not None:
        args.extend(['-F', str(spec.ssh_config)])
    remotecmd = '%s -c "%s"' % (remotepython, popen_bootstrapline)
    if spec.type == 'vagrant' or spec.type == 'kitchen':
        args.extend([remotecmd])
    else:
        args.extend([spec.ssh, remotecmd])
    return args


execnet.gateway_io.ssh_args = new_ssh_args


class RPCWrapper(object):

    def __init__(self, host):
        self.host = host

    def __getattr__(self, name):
        def call(*args, **kw):
            output.annotate(
                'rpc {}: {}(*{}, **{})'.format(self.host.fqdn, name, args, kw),
                debug=True)
            self.host.channel.send((name, args, kw))
            while True:
                message = self.host.channel.receive()
                output.annotate('{}: message: {}'.format(
                    self.host.fqdn, message), debug=True)
                type = message[0]
                if type == 'batou-result':
                    return message[1]
                elif type == 'batou-output':
                    _, output_cmd, args, kw = message
                    getattr(output, output_cmd)(*args, **kw)
                elif type == 'batou-configuration-error':
                    raise ConfigurationError(None)
                elif type == 'batou-deployment-error':
                    raise DeploymentError()
                elif type == 'batou-unknown-error':
                    output.error(message[1])
                    raise RuntimeError(
                        '{}: Remote exception encountered.'.format(
                            self.host.fqdn))
                elif type == 'batou-error':
                    # Remote put out the details already.
                    raise RuntimeError(
                        '{}: Remote exception encountered.'.format(
                            self.host.fqdn))
                else:
                    raise RuntimeError(
                        "{}: Unknown message type {}".format(
                            self.host.fqdn, type))
        return call


_no_value_marker = object()


class Host(object):

    ignore = False
    platform = None

    def __init__(self, fqdn, environment):
        self.fqdn = fqdn
        self.name = self.fqdn.split('.')[0]
        self.data = {}

        self.rpc = RPCWrapper(self)
        self.environment = environment

    def deploy_component(self, component, predict_only):
        self.rpc.deploy(component, predict_only)

    def roots_in_order(self):
        return self.rpc.roots_in_order()

    @property
    def components(self):
        return self.environment.components_for(self)


class LocalHost(Host):

    def connect(self):
        self.gateway = execnet.makegateway(
            "popen//python={}".format(sys.executable))
        self.channel = self.gateway.remote_exec(remote_core)

    def start(self):
        # Since we reconnected, any state on the remote side has been lost,
        # so we need to set the target directory again (which we only can
        # know about locally).
        self.rpc.setup_output()

        env = self.environment
        self.remote_repository, self.remote_base = (
            self.rpc.ensure_target_and_base(
                os.getcwd(), '.'))

        # XXX the cwd isn't right.
        self.rpc.setup_deployment(
            env.name, self.fqdn,
            env.overrides, env.deployment.timeout, env.deployment.platform)

    def disconnect(self):
        if hasattr(self, 'gateway'):
            self.gateway.exit()


class RemoteHost(Host):

    gateway = None

    def connect(self, interpreter='python2.7'):
        if self.gateway:
            output.annotate('Reconnecting ...', debug=True)
            self.disconnect()

        try_sudo = False
        try:
            self.gateway = execnet.makegateway(
                "ssh={}//python={}//type={}".format(
                    self.fqdn, interpreter,
                    self.environment.connect_method))
            self.channel = self.gateway.remote_exec(remote_core)
            if not self.environment.service_user:
                self.environment.service_user = self.rpc.whoami()
            elif self.rpc.whoami() != self.environment.service_user:
                try_sudo = True
                self.disconnect()
        except IOError:
            try_sudo = True

        if try_sudo:
            output.annotate('Trying to switch to sudo ...', debug=True)
            # Call sudo, ensuring:
            # - no password will ever be asked (fail instead)
            # - we ensure a consistent set of environment variables
            #   irregardless of the local configuration of env_reset, etc.
            self.gateway = execnet.makegateway(
                "ssh={}//python=sudo -ni -u {} {}//type={}".format(
                    self.fqdn,
                    self.environment.service_user,
                    interpreter,
                    self.environment.connect_method))
            self.channel = self.gateway.remote_exec(remote_core)

        output.annotate('Connected ...', debug=True)

    def start(self):
        output.step(self.name, 'Bootstrapping ...', debug=True)

        env = self.environment
        self.remote_repository, self.remote_base = (
            self.rpc.ensure_target_and_base(
                env.target_directory, env.deployment_base))
        self.rpc.lock()
        self.rpc.ensure_repository(env.update_method)

        output.step(self.name, 'Updating repository ...', debug=True)
        env.repository.update(self)

        bootstrap = generate_bootstrap(env.version, env.develop)

        args = []
        if env.deployment.fast:
            args.append('--fast')
        if env.deployment.reset:
            args.append('--reset')
        self.rpc.build_batou(bootstrap, args)

        # Now, replace the basic interpreter connection, with a "real" one
        # that has all our dependencies installed.
        self.connect(self.remote_base + '/.batou/bin/python')

        # We need to re-establish state from the new connection.
        self.remote_repository, self.remote_base = (
            self.rpc.ensure_target_and_base(
                env.target_directory, env.deployment_base))
        self.rpc.lock()

        # Since we reconnected, any state on the remote side has been lost,
        # so we need to set the target directory again (which we only can
        # know about locally)
        self.rpc.setup_output()

        self.rpc.setup_deployment(
            env.name,
            self.fqdn,
            env.overrides, env.deployment.timeout, env.deployment.platform)

    def disconnect(self):
        if self.gateway is not None:
            self.rpc.unlock()
            self.gateway.exit()
