import datetime
import logging
import os
import select
import shutil
import time

from subprocess import PIPE, STDOUT, Popen

from zymbit.compat import config
from zymbit.exceptions import TunnelError
from zymbit.timeutil import now

SSH = os.environ.get('SSH', '/usr/bin/ssh')
MAX_BACKOFF = 300  # 5 minutes

# how long a connection must be in place in order to consider it okay to clear backoff
CONNECTION_OK_DELTA = datetime.timedelta(seconds=30)


class Tunnel(object):
    defaults = {
        'user': None,
        'host': None,
        'identity_file': None,
        'ssh_port': None,
        'port': 22,
    }

    def __init__(self, **kwargs):
        _config = self.defaults.copy()
        _config.update(config.get_config().get('tunnel', {}))
        _config.update(dict([item for item in kwargs.items() if item[1]]))

        unconfigured = [key for key, val in _config.items() if val is None]
        if unconfigured:
            raise TunnelError('{} cannot be unset'.format(unconfigured))

        self.user = _config['user']
        self.host = _config['host']
        self.port = str(_config['port'])
        self.identity_file = _config['identity_file']
        self.ssh_port = str(_config['ssh_port'])

        self.connection = None
        self.connected_at = None

        self.backoff = 0
        self.backoff_until = None

        self.connect()

    @property
    def logger(self):
        return logging.getLogger(__name__)

    def close(self):
        if self.connection is None:
            return

        try:
            self.connection.kill()
        except OSError, exc:
            if exc[0] != 3:
                raise

        self.connection.wait()

        self.connection = None

    def check(self):
        if not self.connection:
            self.connect()
            if self.connection is None:
                self.logger.debug('no connection, sleeping 1.0s')
                time.sleep(1.0)
                return

        # this is the main throttling sleep here, keep it at around 1 second
        stdout = self.connection.stdout
        r, _, _ = select.select([stdout], [], [], 1.0)
        for stream in r:
            line = stream.readline().strip()
            if line:
                self.logger.info('line={}'.format(line))

        status = self.connection.poll()
        if status is None:
            # connection is established, see if backoff needs to be reset
            if self.backoff and now() > self.connected_at + CONNECTION_OK_DELTA:
                self.logger.debug('connection ok, resetting backoff')
                self.backoff = 0
                self.backoff_until = None

            return

        # a connection had been established, but has ended, need to reconnect
        self.logger.warning('closing connection after status={}'.format(status))
        self.close()

        # set the backoff time
        self.backoff += 1
        seconds = min(2**self.backoff, MAX_BACKOFF)
        self.backoff_until = now() + datetime.timedelta(seconds=seconds)

        self.logger.debug('backoff={}, seconds={}, backoff_until={}'.format(
            self.backoff, seconds, self.backoff_until))

    def check_host_key(self):
        zymbit_known_hosts = config.ZYMBIT_KNOWN_HOSTS
        known_hosts = config.KNOWN_HOSTS
        if not os.path.exists(known_hosts):
            shutil.copyfile(zymbit_known_hosts, known_hosts)
            return True

        with open(zymbit_known_hosts, 'rb') as zbkh:
            while True:
                pubkey = zbkh.readline()
                if pubkey == '':  # end of file
                    break

                pubkey = pubkey.strip()
                if pubkey == '':  # skip empty lines
                    continue

                known_hosts_temp = '{}.tmp'.format(known_hosts)

                try:
                    line_found = False
                    with open(known_hosts_temp, 'wb') as new_hosts:
                        with open(known_hosts, 'rb') as old_hosts:
                            line = old_hosts.readline().strip()

                            if line == pubkey:
                                line_found = True

                            new_hosts.write('{}\n'.format(line))

                        if not line_found:
                            new_hosts.write('{}\n'.format(pubkey))

                    # overwrite the temporary file
                    shutil.move(known_hosts_temp, known_hosts)
                finally:
                    if os.path.exists(known_hosts_temp):
                        os.remove(known_hosts_temp)

    def connect(self):
        if self.backoff_until and self.backoff_until > now():
            self.logger.debug('not connecting, backoff_until={}'.format(self.backoff_until))
            return

        self.check_host_key()

        self.connection = Popen((
            SSH, '-N', '-I', '60', '-R', self.tunnel_conf,
            '-i', self.identity_file, '-p', self.port, self.remote_server
        ), stdout=PIPE, stderr=STDOUT)

        self.connected_at = now()

        return self.connection

    def loop(self):
        return self.check()

    def reconnect(self):
        self.close()
        self.connect()

    @property
    def remote_server(self):
        return '{}@{}'.format(self.user, self.host)

    @property
    def tunnel_conf(self):
        return '{}:localhost:22'.format(self.ssh_port)
