import argparse
import subprocess
import logging
import socket
import sys
import configparser

from datetime import datetime, timedelta
from os.path import join, isdir, isfile

from paramiko import SSHClient


logger = logging.getLogger('ubac')
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))


class UbacBase(object):
    def __init__(self, ssh_host='localhost', ssh_port=22, ssh_user='ubac',
                 dest_base_path='/var/backups'):
        self.ssh_host = ssh_host
        self.ssh_port = ssh_port
        self.ssh_user = ssh_user
        self.dest_base_path = dest_base_path

        self.parent_dir = join(
            self.dest_base_path, socket.gethostname(), self.DEST_SUBDIR,
        )

    @property
    def ssh(self):
        if not hasattr(self, '_ssh'):
            self._ssh = SSHClient()
            self._ssh.load_system_host_keys()
            self._ssh.connect(
                self.ssh_host, port=self.ssh_port, username=self.ssh_user
            )
        return self._ssh

    def close(self):
        if not hasattr(self, '_ssh'):
            return
        self._ssh.close()
        del self._ssh

    def list_dir(self, directory):
        stdin, stdout, stderr = self.ssh.exec_command(
            'ls %s' % (directory)
        )
        olders = stdout.readlines()
        olders = map(lambda o: o.replace('\n', ''), olders)
        olders = sorted(olders)
        return olders


class UbacPostgresql(UbacBase):
    DEST_SUBDIR = 'postgresql'

    def __init__(self, **kwargs):
        super(UbacPostgresql, self).__init__(**kwargs)

    def list_dbs(self):
        cmd = [
            'psql', 'postgres', '-t', '-c',
            "SELECT datname FROM pg_database WHERE datistemplate = false"
        ]
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        assert p.wait() == 0
        for dbname in p.stdout.readlines():
            dbname = dbname.strip()
            if dbname:
                yield dbname
            else:
                return

    def dump_db(self, dbname):
        cmd = ["pg_dump", "-x", "-O", dbname]
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        for line in p.stdout:
            yield line
        assert p.wait() == 0

    def single_db(self, dbname):
        logger.info('backuping pg db %s', dbname)
        dest_dir = join(self.parent_dir, dbname)
        self.ssh.exec_command('mkdir -p ' + dest_dir)
        dest = join(
            dest_dir,
            datetime.utcnow().strftime('%Y%m%d_%H%M%S') + '.sql.gz'
        )

        stdin, stdout, stderr = self.ssh.exec_command('gzip - > %s' % (dest))
        for line in self.dump_db(dbname):
            stdin.write(line)
        stdin.close()

    def all(self):
        for dbname in self.list_dbs():
            try:
                self.single_db(dbname.decode('utf-8'))
            except KeyboardInterrupt:
                sys.exit(-1)
            except:
                logger.exception('Error backuping pg db %s', dbname)

    def check_all(self, freshness=24):
        for dbname in self.list_dbs():
            try:
                self.check(dbname.decode('utf-8'), freshness)
            except AssertionError as e:
                logging.error(*e.args)

    def check(self, dbname, freshness=24):
        dest_dir = join(self.parent_dir, dbname)
        backups = self.list_dir(dest_dir)
        assert backups, "No backup found for pg db %s" % (dbname)
        last = backups[-1]
        last_date = datetime.strptime(last.split('.', 2)[0], '%Y%m%d_%H%M%S')
        last_age = datetime.utcnow() - last_date
        assert last_age < timedelta(hours=freshness), (
            'last backup for pg db %s is %s' % (dbname, last_age)
        )
        logger.info('Backup of pg db %s is fresh enough', dbname)


FS_DEFAULT_PATHS = [
    '/etc/',
    '/home/',
    '/root',
    '/srv',
    '/usr/local',
    '/var/log',
]


class UbacFs(UbacBase):
    DEST_SUBDIR = 'fs'

    def __init__(self, *args, **kwargs):
        super(UbacFs, self).__init__(**kwargs)

        if not args:
            self.paths = FS_DEFAULT_PATHS
        else:
            self.paths = []
        for path in args:
            path = path.strip()
            if path:
                self.paths.append(path)

        self.now = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
        self.dest_base_dir = join(self.parent_dir, self.now)

        self.dest_initialised = False

    def _init_dest(self):
        if self.dest_initialised:
            return
        self.ssh.exec_command('mkdir -p ' + self.parent_dir)
        olders = self.list_dir(self.parent_dir)
        if not olders:
            self.ssh.exec_command('mkdir -p ' + self.dest_base_dir)
            self.dest_initialised = True
            return
        last = olders[-1]

        cmd = 'cp -al %s %s' % (
            join(self.parent_dir, last),
            self.dest_base_dir
        )
        self.ssh.exec_command(cmd)
        self.dest_initialised = True

    def all(self):
        self._init_dest()
        for path in self.paths:
            try:
                self.single_path(path)
            except KeyboardInterrupt:
                sys.exit(-1)
            except:
                logger.exception('Error backuping fs path %s', path)

    def single_path(self, path):
        logger.info('backuping path %s', path)
        if not isdir(path):
            logger.warning('Path %s does not exist ; ignoring it', path)
            return
        self._init_dest()

        index = 0
        if path[0] == '/':
            index = 1

        dest_path = join(self.dest_base_dir, path[index:])
        self.ssh.exec_command('mkdir -p ' + dest_path)

        command = 'rsync -az -e ssh --delete'.split()
        command += [path + '/', '%s@%s:%s/' % (
            self.ssh_user, self.ssh_host, dest_path
        )]
        subprocess.check_call(command)

    def check_all(self, freshness=24):
        for path in self.paths:
            try:
                self.check(path, freshness=freshness)
            except AssertionError as e:
                logger.error(*e.args)

    def check(self, path, freshness=24):
        backups = self.list_dir(self.parent_dir)
        assert backups, 'No backup found for path %s' % (path)
        last = backups[-1]
        last_date = datetime.strptime(last.split('.', 2)[0], '%Y%m%d_%H%M%S')
        last_age = datetime.utcnow() - last_date
        assert last_age < timedelta(hours=freshness), (
            'last backup for path %s is %s' % (path, last_age)
        )
        logger.info('Backup of path %s is fresh enough', path)


def get_params():
    parser = argparse.ArgumentParser(description='Backup some data')
    parser.add_argument(
        '--config', dest="config", help="Read configuration from that file",
        default=''
    )
    args, remaining_argv = parser.parse_known_args()
    defaults = {
        'dest_base_path': '/var/backups',
        'ssh_host': 'localhost',
        'ssh_port': 22,
        'ssh_user': 'ubac',
        'no_pg': False,
        'pg_dbs': [],
        'no_fs': False,
        'fs_paths': FS_DEFAULT_PATHS,
        'check_since': 24,
        'check': False,
    }

    filename = None
    if args.config:
        if isfile(args.config):
            filename = args.config
        else:
            raise FileNotFoundError('File %s does not exists' % (args.config))

    default_config_file = '/etc/ubac.conf'
    if not filename and isfile(default_config_file):
        filename = default_config_file

    if filename:
        config = configparser.SafeConfigParser()
        config.read([filename])
        defaults.update(dict(config.items("ubac")))
    for key in ('fs_paths', 'pg_dbs'):
        if type(defaults[key]) == str:
            defaults[key] = defaults[key].split('\n')

    parser = argparse_arguments(parser)
    parser.set_defaults(**defaults)
    return parser.parse_args(remaining_argv)


def argparse_arguments(parser):
    parser.add_argument(
        '-d', '--dest-base-path', dest="dest_base_path",
        help='Destination base dir'
    )
    parser.add_argument(
        '--ssh-host', dest='ssh_host', help='SSH host'
    )
    parser.add_argument(
        '--ssh-port', dest='ssh_port', type=int, help='SSH port'
    )
    parser.add_argument(
        '--ssh-user', dest='ssh_user', help='SSH user'
    )
    parser.add_argument(
        '--check', dest='check', action="store_true",
        help='SSH user'
    )
    parser.add_argument(
        '--no-pg', dest='no_pg', action="store_true",
        help='disable postgresql backup'
    )
    parser.add_argument(
        '--pg-dbs', dest='pg_dbs', nargs='*', type=str,
        help='Postgresql databases to backup'
    )
    parser.add_argument(
        '--no-fs', dest='no_fs', action="store_true",
        help='disable filesystem backup'
    )
    parser.add_argument(
        '--fs-paths', dest='fs_paths', nargs='*', type=str,
        help='Paths to backup'
    )
    parser.add_argument(
        '--check-since', dest="check_since", type=int,
        help='hours since last backup declaring a resource not fresh enough'
    )
    return parser


def main():
    args = get_params()
    common_args = dict(
        ssh_host=args.ssh_host,
        ssh_port=args.ssh_port,
        ssh_user=args.ssh_user,
        dest_base_path=args.dest_base_path
    )

    if not args.no_fs:
        ubac_fs = UbacFs(*args.fs_paths, **common_args)
        if args.check:
            ubac_fs.check_all(args.check_since)
        else:
            ubac_fs.all()
        ubac_fs.close()

    if not args.no_pg:
        ubac_pg = UbacPostgresql(**common_args)
        if args.check:
            ubac_pg.check_all(args.check_since)
        else:
            ubac_pg.all()
        ubac_pg.close()


if __name__ == '__main__':
    main()
