#!/usr/bin/env python
##
#
# Vaisala software source code file
#
# Copyright (c) Vaisala Oyj 2015. All rights reserved.
#
##
from __future__ import print_function

import StringIO
import argparse
import logging
import os
import pwd
import subprocess
import sys
import time
import traceback
import threading


LINE_LENGTH = 78
_logger = logging.getLogger('rsw-basemap-installer')
CONFIG_DIRECTORY = os.environ.get("VAISALA_RADARSW_CONFIG_DIR",
                                  '/etc/vaisala/radarsw/configuration')


class Dotter(object):
    def __init__(self, msg):
        self.stop_running = threading.Event()
        self.thread = threading.Thread(target=self.init_dotter)
        self.msg = msg
        self.max_dots = LINE_LENGTH - len(msg) - 1

    def start(self):
        self.thread.start()

    def stop(self):
        self.stop_running.set()
        self.thread.join()

    def init_dotter(self):
        dots = 0
        while not self.stop_running.is_set():
            if dots > self.max_dots:
                sys.stderr.write(u"\r\033[K" + self.msg)
                dots = 0
            sys.stderr.write(u".")
            sys.stderr.flush()
            time.sleep(0.25)
            dots += 1


class StepExecutor(object):
    def __init__(self, steps, logger, dry_run):
        self._steps = steps
        self._logger = logger
        self._dry_run = dry_run

    def execute(self):
        for idx, step in enumerate(self._steps):
            log(u"# Step {}/{}\n".format(idx + 1, len(self._steps)))
            msg = u"## " + str(step)
            log(msg)
            msg_len = len(msg)
            dots = LINE_LENGTH - msg_len - 4
            self._logger.debug(msg)

            while True:
                try:
                    if self._dry_run:
                        log((u"\r" + msg + u"." * dots) + u"SKIP\n")
                    else:
                        dotter = Dotter(msg)
                        dotter.start()
                        step()
                        dotter.stop()
                        log((u"\r" + msg + u"." * dots) + u"DONE\n")
                    break
                except SystemExit, se:
                    dotter.stop()
                    log((u"\r" + msg + u"." * dots) + u"FAIL")
                    sys.exit(*se.args)
                except StepFailed, f:
                    try:
                        if dotter:
                            dotter.stop()

                        log((u"\r" + msg + u"." * dots) + u"FAIL\n")
                        for line in step.buffer:
                            self._logger.info(line)
                        self._logger.info(u"Step failed: {0}".format(str(f)))
                        if step.help_message is not None:
                            self._logger.info(step.help_message)
                        action = self._skip_retry_fail()
                        if action == 'skip':
                            break
                        elif action == 'retry':
                            pass
                        elif action == 'fail':
                            raise f
                    except KeyboardInterrupt, kbi:
                        self._logger.error(traceback.format_exc())
                        raise kbi
                finally:
                    if not self._dry_run and dotter:
                        dotter.stop()

    def _skip_retry_fail(self):
        allowed_inputs = ['skip', 'retry', 'fail']
        while True:
            self._logger.info('Skip, retry or fail step (skip/retry/fail)?')
            reply = raw_input().replace('\n', '').strip()
            if reply not in allowed_inputs:
                self._logger.info(u"Allowed inputs are {0}.".format(", ".join(allowed_inputs)))
            else:
                break
        return reply


def dbg(*args, **kwargs):
    buf = StringIO.StringIO()
    kwargs['file'] = buf
    print(*args, **kwargs)
    _logger.debug(buf.getvalue().rstrip('\n'))
    buf.close()


def log(*args, **kwargs):
    buf = StringIO.StringIO()
    kwargs['file'] = buf
    print(*args, **kwargs)
    _logger.info(buf.getvalue().rstrip('\n'))
    buf.close()


class StepFailed(Exception):
    pass


class Step(object):
    def __init__(self, prerequisite=False, help_message=None):
        self.prerequisite = prerequisite
        self.help_message = help_message
        self.buffer = []

    def __str__(self):
        return self.description


class FunctionStep(Step):
    def __init__(self, func, description, *args, **kwargs):
        super(FunctionStep, self).__init__(*args, **kwargs)
        self.func = func
        self.description = description

    def __call__(self):
        return self.func()


class ShellCommandStep(Step):
    def __init__(self, command, description, logger, shell=False, *args, **kwargs):
        super(ShellCommandStep, self).__init__(*args, **kwargs)
        self.command = command
        self.description = description
        self.shell = shell
        self._logger = logger

    def printable_command(self):
        if isinstance(self.command, basestring):
            return self.command
        else:
            return " ".join(self.command)

    def __call__(self):
        self._logger.debug(u"Running command: '" + self.printable_command() + u"'")
        proc = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=self.shell)

        def log_stream_lines(stream):
            for line in iter(stream.readline, b''):
                self._logger.debug(line.rstrip('\n'))
                self.buffer.append(line.rstrip('\n'))

        while proc.poll() is None:
            log_stream_lines(proc.stdout)
            log_stream_lines(proc.stderr)
            time.sleep(0.01)

        # Handle lines possibly left in the buffers
        log_stream_lines(proc.stdout)
        log_stream_lines(proc.stderr)

        status = proc.returncode
        if status != 0:
            raise StepFailed(
                u"Command `{0}` failed with status code {1}".format(
                    self.printable_command(), status))


def skip_retry_fail():
    allowed_inputs = ['skip', 'retry', 'fail']

    while True:
        log('Skip, retry or fail step (skip/retry/fail)?')
        reply = raw_input().replace('\n','').strip()
        if reply not in allowed_inputs:
            log(u"Allowed inputs are {0}.".format(", ".join(allowed_inputs)))
        else:
            break

    return reply


def create_arg_parser():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("--gis-db-dump", dest="gis_db_dump", metavar="PATH", help="GIS database dump.", required=True)

    terrain_group = parser.add_mutually_exclusive_group(required=True)
    terrain_group.add_argument("--terrain-dir", dest="terrain_dir", metavar="DIRECTORY", default=None,
                               help="Terrain directory.")
    terrain_group.add_argument("--skip-terrain", dest="skip_terrain", action="store_true", default=False,
                               help="Skip terrain layer.")

    parser.add_argument("--max-zoom", dest="max_zoom", type=int, help=argparse.SUPPRESS)
    parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug messages.")

    opt_group = parser.add_argument_group("optional arguments")
    opt_group.add_argument("--dry-run", dest="dry_run", action="store_true", default=False,
                           help="Don't do anything, just list the steps that would be run.")

    return parser


def validate_args(args, parser):
    if not os.path.exists(args.gis_db_dump):
        parser.error(u"GIS database dump '{}' does not exist.".format(args.gis_db_dump))

    if not args.skip_terrain and not os.path.exists(args.terrain_dir):
        parser.error(u"Terrain directory '{}' does not exist.".format(args.terrain_dir))


def setup_logging(debug, dry_run):
    _logger.setLevel(logging.DEBUG)

    sout_h = logging.StreamHandler(sys.stdout)
    if debug:
        sout_h.setLevel(logging.DEBUG)
    else:
        sout_h.setLevel(logging.INFO)
    _logger.addHandler(sout_h)

    log_file = os.path.join(os.environ['HOME'], u"rsw-basemap-installer-{0}.log".format(time.strftime("%Y%m%d-%H%M%S")))
    file_h = logging.FileHandler(log_file)
    file_h.setLevel(logging.DEBUG)
    _logger.addHandler(file_h)

    if not dry_run:
        log(u"Logging to stdout and to '{0}'.".format(log_file))
    return log_file


def create_installation_steps(args):
    steps = [
        ShellCommandStep(['service', 'vaisala-radarsw-geoserver', 'start'],
                         u"Start GeoServer (or make sure it's running)", logger=_logger),
        ShellCommandStep(['rsw-http-wait-until-healthy', '-u', 'http://localhost:34180/geoserver/',
                          '-c', '302', '-n', 'GeoServer'], u"Waiting for GeoServer to start up", logger=_logger),

        ShellCommandStep(['rsw-geoserver-password-tool', 'reset-master-password'], u"Change GeoServer master password",
                         logger=_logger),
        ShellCommandStep(['service', 'vaisala-radarsw-geoserver', 'stop'],
                         u"Stop GeoServer to take new admin password and plugins into use", logger=_logger),

        ShellCommandStep(['rsw-geoserver-password-tool', 'reset-admin-password'], u"Change GeoServer admin password",
                         logger=_logger),
        ShellCommandStep(['rsw-basemap-install-plugins'], u"Install GeoServer plugins", logger=_logger),
        ShellCommandStep(['rsw-basemap-install-configuration'], u"Install GeoServer configuration", logger=_logger),
        ShellCommandStep(['rsw-basemap-install-resources'], u"Install GeoServer resources", logger=_logger),
        ShellCommandStep(['rsw-geoserver-configure-logging'], u"Configure GeoServer production logging", logger=_logger)
    ]

    if not args.skip_terrain:
        steps.append(
            ShellCommandStep(['rsw-basemap-install-terrain-files', args.terrain_dir], u"Install terrain files",
                             logger=_logger))

    finalization_steps = [
        ShellCommandStep(['service', 'vaisala-radarsw-geoserver', 'restart'],
                         u"Restart GeoServer to take changes into effect", logger=_logger),
        ShellCommandStep(['rsw-http-wait-until-healthy', '-u', 'http://localhost:34180/geoserver/',
                          '-c', '302', '-n', 'GeoServer'], u"Waiting for GeoServer to start up", logger=_logger),
        ShellCommandStep(['rsw-gis-db-tool', 'create'], u"Create GIS database", logger=_logger),
        ShellCommandStep(['rsw-gis-db-tool', 'restore-dump', '--gis-db-dump', args.gis_db_dump],
                         u"Populate GIS database", logger=_logger),
        ShellCommandStep(['rsw-gis-db-tool', 'vacuum'], u"Run maintenance on GIS database", logger=_logger),
    ]

    if args.max_zoom is not None:
        cmd = ['rsw-basemap-layer-setup', '--max-zoom', str(args.max_zoom)]
        if args.skip_terrain:
            cmd.append('--skip-terrain')

        finalization_steps.append(
            ShellCommandStep(cmd, u"Create layers for GeoServer (with specified max zoom)", logger=_logger))
    else:
        cmd = ['rsw-basemap-layer-setup']
        if args.skip_terrain:
            cmd.append('--skip-terrain')

        finalization_steps.append(ShellCommandStep(cmd, u"Create layers for GeoServer", logger=_logger))

    return steps + finalization_steps


def log_system_information():
    dbg("###")
    dbg("## Begin system information section")
    dbg("###")

    def no_check_output(*args, **kwargs):
        try:
            return subprocess.check_output(*args, **kwargs)
        except Exception, e:
            return str(e)

    dbg("Installed packages with postgres|postgis|vaisala in their name:")
    dbg(no_check_output(['yum list installed | grep postgres'], shell=True))
    dbg(no_check_output(['yum list installed | grep postgis'], shell=True))
    dbg(no_check_output(['yum list installed | grep vaisala'], shell=True))
    dbg("Current directory: {}".format(os.getcwd()))
    dbg("Started with command line {}".format(" ".join(sys.argv)))


def log_title(title):
    log(u"#" * LINE_LENGTH)
    log(u"# " + title)
    log(u"#" * LINE_LENGTH)


def main():
    parser = create_arg_parser()
    args = parser.parse_args()
    validate_args(args, parser)

    log_file = setup_logging(args.debug, args.dry_run)
    log_system_information()

    if pwd.getpwuid(os.getuid()).pw_name != 'root':
        sys.exit("This script must be run as root.")

    steps = create_installation_steps(args)
    status = u"FAILED"
    try:
        log_title(u"Basemap installation")
        StepExecutor(steps, _logger, args.dry_run).execute()
        status = u"SUCCESSFUL"
    finally:
        if args.dry_run:
            pass
            # log_title(u"# Dry run complete")
        else:
            log(u"\n" + (u"#" * LINE_LENGTH))
            log(u"# Installation " + status + u". Log: {}".format(log_file))
            log(u"#" * LINE_LENGTH)

if __name__ == '__main__':
    main()

