#!/usr/bin/env python
# -*- coding: utf-8 -*-
##
#
# Vaisala software source code file
#
# Copyright (c) Vaisala Oyj 2016. All rights reserved.
#
##
import argparse
import ConfigParser
import cookielib
import datetime
import httplib
import json
import socket
import urllib
import urllib2
import urlparse

from installer_utils import *

_logger = logging.getLogger('rsw-installer')

DATA_SECTION = "DATASOURCE"
DATA_URL_OPTION = "datasource.url"
DATA_UNAME_OPTION = "datasource.username"
DATA_PWD_OPTION = "datasource.password"


def log_installed_vaisala_radarsw_packages():
    try:
        dbg("Currently installed vaisala-radarsw packages:")
        dbg(
            subprocess.check_output(
                "rpm -qa --qf '%{NAME} %{VERSION}\n' | grep vaisala-radarsw-",
                shell=True))
    except:
        pass


def log(*args, **kwargs):
    return _logger.info(*args, **kwargs)


def dbg(*args, **kwargs):
    return _logger.debug(*args, **kwargs)


# Module / program level variable, initialized in main.
# Contains shared state by Yum repo steps.
REPO_STATE = None
BASE_REPO_SETUP_STEP = FunctionStep(lambda: base_repo_setup(REPO_STATE, _logger),
                                    u"Take CentOS base repository into use if needed")  # TODO: help_message
BASE_REPO_CLEANUP_STEP = FunctionStep(lambda: base_repo_cleanup(REPO_STATE, _logger),
                                      u"Reset Yum repositories to its original state")  # TODO: help_message


def extract_iris_host(file_path):
    """Extracts the data source password from a file and does no error
    handling. User is expected to handle ConfigParser thrown errors.

    See https://docs.python.org/2.7/library/configparser.html#ConfigParser.Error
    for more information.
    """
    config = ConfigParser.SafeConfigParser()
    config.read(file_path)
    return config.get("IRIS_SOCKET_SERVER", "iris.socket.server.host")


def is_iris_host_set(file_path):
    try:
        iris_host = extract_iris_host(file_path)
        if iris_host is not None:
            return True
    except ConfigParser.Error:
        pass

    return False


def resolve_socket_server_address(socket_server_arg, parser, config_file, config_file_rpmsave):
    dbg(u"## Setup step")
    dbg(u"# Determining IRIS Socket Server address")
    dbg(u"##")
    if socket_server_arg is not None:
        return socket_server_arg

    if is_iris_host_set(config_file):
        from_configuration = extract_iris_host(config_file)
        dbg(u"Using the value found from the configuration file ({0}): {1}".format(
            config_file, from_configuration))
        return from_configuration
    elif is_iris_host_set(config_file_rpmsave):
        from_rpmsave = extract_iris_host(config_file_rpmsave)
        dbg(u"Using the value found from the .rpmsave file ({0}): {1}".format(
            config_file_rpmsave, from_rpmsave))
        return from_rpmsave
    else:
        parser.error(u"Cannot determine IRIS Socket Server address. Missing -s option?")


def socket_server_version_check(host):
    server_port = 30735

    def make_request(command, params={}):
        param_parts = []
        for k, v in params.iteritems():
            param_parts.append(u"{0}={1}".format(k, v))
        params = "&".join(param_parts)

        length = len(params) + 1 + len(command)
        return str(length).zfill(9) + command + '|' + params

    version_request = make_request("SITE_VERSION")

    def read_bytes(sock, count):
        bytes_to_read = count
        bytes_received = 0
        buf = []
        while bytes_received < bytes_to_read:
            received = sock.recv(bytes_to_read - bytes_received)
            if received and len(received) > 0:
                buf.append(received)
                bytes_received += len(received)
        return ''.join(buf)

    def read_response_length(sock):
        return int(read_bytes(sock, 9))

    def query_version(sock):
        sock.send(version_request)
        length = read_response_length(sock)
        reply = read_bytes(sock, length)

        ack, payload = reply.split('|')
        payload_parts = payload.split(',')
        version = None
        for part in payload_parts:
            if 'version' in part.lower():
                version = part.split('=')[-1]
        if version is None:
            raise StepFailed(u"Could not parse version from {0}".format(reply))
        return version

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(5)
        sock.connect((host, server_port))
        version = query_version(sock)
    except Exception, e:
        if "Name or service not known" in str(e):
            raise StepFailed(u"Could not resolve IRIS Socket server host name {0}".format(host))
        elif "timed out" in str(e):
            raise StepFailed(
                u"Could not receive anything from IRIS Socket server with host name {0}. \
                Is the host running a socket server?".format(host))

        raise StepFailed(u"Could not connect to host {0}, error:\n{1}".format(
            host, traceback.format_exc()))

    dbg(version)
    if len(version) < 2:
        raise StepFailed("Couldn't figure out IRIS Socket server version")


def install_packages_command():
    packages = [
        'vaisala-radarsw-webapp',
        'vaisala-radarsw-webapp-proxy',
        'vaisala-radarsw-monitoring',
        'vaisala-radarsw-backup',
        'vaisala-radarsw-scan-service'
        # 'vaisala-radarsw-data-manager'
    ]
    return ['yum', '-y', 'install'] + packages


def configure_iris_host_command(socket_server):
    iris_host_command = ["/usr/vaisala/radarsw/configuration/bin/configure-iris-host"]
    if socket_server is not None:
        iris_host_command = iris_host_command + [socket_server]
    return iris_host_command


def wait_until_code(code, netloc, path, target, timeout_secs=90, attempt_delay_secs=1):
    dbg(u"Waiting for {0} to be available...".format(target))
    start = datetime.datetime.now()
    timeout_delta = datetime.timedelta(seconds=timeout_secs)
    while True:
        try:
            conn = httplib.HTTPConnection(netloc, timeout=1)
            conn.request("HEAD", path)
            res = conn.getresponse()

            if res.status == code:
                now = datetime.datetime.now()
                waited = (now - start).total_seconds()
                dbg(u"\n{0} up in {1} seconds.".format(target, waited))
                break
            else:
                dbg(res.status)
        except Exception, e:
            dbg(e)

        now = datetime.datetime.now()
        if (start + timeout_delta) < now:
            waited = (now - start).total_seconds()
            sys.stderr.write('\n')
            sys.stderr.flush()
            raise StepFailed(
                u"Timed out waiting for {0} to start up, waited for {1} seconds.".format(target, waited))

        time.sleep(attempt_delay_secs)


def set_in_env(name, value):
    if "pwd" in name or "password" in name:
        dbg("Setting in env: {0} = {1}".format(name, value[:2] + "*****"))
    else:
        dbg("Setting in env: {0} = {1}".format(name, value))
    os.environ[name] = value


def file_exists(file):
    dbg(u"Checking whether '{0}' exists.".format(file))
    if not os.path.exists(file):
        raise StepFailed(u"Path '{0}' doesn't exist!".format(file))
    dbg(u"Path '{}' exists.".format(file))


def validate_terrain_dir(terrain_dir):
    file_exists(terrain_dir)
    shade_dir = os.path.join(terrain_dir, "shade_pyramid")
    slope_dir = os.path.join(terrain_dir, "slope_pyramid")
    if not os.path.exists(shade_dir) or not os.path.exists(slope_dir):
        raise StepFailed(
            u"Could not locate subdirectories shade_pyramid and slope_pyramid in '{0}'. \
            Check that you are pointing to a valid terrain directory.".format(
                terrain_dir))
    dbg(u"Path '{0}' points to a valid terrain directory.".format(terrain_dir))


def validate_gis_dump(gis_dump_path):
    file_exists(gis_dump_path)
    if os.path.isdir(gis_dump_path):
        md5sums_file = os.path.join(gis_dump_path, "md5sums.txt")
        if not os.path.exists(md5sums_file):
            raise StepFailed(
                u"Could not locate file md5sums.txt in '{0}'. \
                Check that you are pointing to a valid GIS dump directory.".format(
                    gis_dump_path))
        dbg(u"Path '{0}' points to a valid GIS dump directory.".format(gis_dump_path))
    else:
        dbg(u"Path '{0}' points to a valid GIS dump file.".format(gis_dump_path))


def make_remove_postgres_92_packages_step():
    non_empty_lines = [line.strip()
                       for line in
                       subprocess.check_output("rpm -qa --qf '%{NAME}\t%{VERSION}\n'",
                                               shell=True).split("\n")
                       if len(line.strip()) > 0]

    postgres_lines = [line for line in non_empty_lines if 'postgres' in line]
    package_version_pairs = [line.split("\t") for line in postgres_lines]

    postgres_92_packages = []
    for package_name, version in package_version_pairs:
        if '9.2' in version:
            postgres_92_packages.append(package_name)

    if len(postgres_92_packages) > 0:
        return ShellCommandStep(["yum", "-y", "remove"] + postgres_92_packages,
                                u"Remove PostgreSQL 9.2 packages", logger=_logger)
    else:
        return FunctionStep(lambda: dbg("No PostgreSQL packages to remove."),
                            u"Remove PostgreSQL 9.2 packages")


def can_log_in_as_admin(password):
    login_url = "http://127.0.0.1:34080" + "/login"
    cookie_jar = cookielib.CookieJar()

    def fail():
        raise StepFailed(
            u"Could not authenticate as user 'admin' with given password. \
            Try set admin password with --admin-password parameter.")

    try:
        handlers = [urllib2.HTTPHandler(), urllib2.HTTPCookieProcessor(cookie_jar)]
        opener = urllib2.build_opener(*handlers)
        data = {'username': 'admin', 'password': password}
        response = opener.open(login_url, urllib.urlencode(data)).read()
        content = json.loads(response)
        if content['authenticated'] is True:
            dbg("Succesfully authenticated as user 'admin'.")
            return

        dbg(repr(content))
        fail()
    except Exception, e:
        dbg("Login as user 'admin' failed.")
        dbg(traceback.format_exc())
        fail()


def make_gis_dump_existence_check_step(gis_dump_path):
    return PreFunctionStep(lambda: validate_gis_dump(gis_dump_path),
                           u"Validate GIS dump",
                           help_message=u"""Check that GIS dump directory (or file) '{}' points to a valid location.
If you are pointing to a directory, it should contain the md5sums.txt file.
""".format(gis_dump_path))


def make_terrain_dir_existence_check_step(terrain_dir):
    return PreFunctionStep(lambda: validate_terrain_dir(terrain_dir),
                           u"Validate terrain directory",
                           help_message=u"""Check that terrain directory '{}' points to a valid location.
The terrain directory should contain subdirectories named 'shade_pyramid' and 'slope_pyramid'.""".format(terrain_dir))


def create_basemap_installation_steps(gis_db_dump, max_zoom, terrain_dir):
    result = [ShellCommandStep(['yum', '-y', 'install', 'vaisala-radarsw-gis'],
                               u"Install basemap dependencies", logger=_logger)]

    basemap_installer_command = ["rsw-basemap-installer", "--gis-db-dump", gis_db_dump]

    if terrain_dir is None:
        basemap_installer_command += ['--skip-terrain']
    else:
        basemap_installer_command += ['--terrain-dir', terrain_dir]

    if max_zoom is not None:
        basemap_installer_command += ['--max-zoom', str(max_zoom)]

    result.append(SubInstallerStep(basemap_installer_command, "Install basemap server", logger=_logger))

    return result


def create_geoserver_site_config_steps(socket_server_address, geoserver_config_url):
    parsed = urlparse.urlparse(geoserver_config_url)
    result = [
        FunctionStep(lambda: wait_until_code(302, parsed.netloc, parsed.path,
                                             "GeoServer", timeout_secs=300),
                     "Wait for basemap GeoServer to start up")
    ]

    # GeoServer site setup
    if "localhost" in geoserver_config_url:
        result.append(
            ShellCommandStep(['rsw-basemap-site-setup',
                              '--socket-server', socket_server_address],
                             u"Configure radar sites for local GeoServer", logger=_logger))
    else:
        result.append(
            ShellCommandStep(["/usr/vaisala/radarsw/configuration/bin/configure-geoserver",
                              "get_centers", "-s", socket_server_address],
                             "Fetch radar centers from socket server", logger=_logger))
        result.append(
            ShellCommandStep(["/usr/vaisala/radarsw/configuration/bin/configure-geoserver",
                              "configure", "-url", geoserver_config_url],
                             "Configure remote GeoServer with correct radar centers", logger=_logger))

    result.append(
        ShellCommandStep(["service", "vaisala-radarsw-webapp", "restart"],
                         "Restart IRIS Focus after site configuration", logger=_logger))
    result.append(
        FunctionStep(lambda: wait_until_code(200, "localhost:34080", "/health", "IRIS Focus"),
                     "Wait for IRIS Focus to start up"))

    return result


def log_system_information_and_arguments(args):
    log_system_information(_logger)
    dbg("Using {} as the IRIS socket server address.".format(args.socket_server))
    dbg("Using {} as the basemap WMS URL.".format(args.wms_url))
    dbg("Using {} as the GeoServer configuration URL.".format(args.geoserver_config_url))


def create_prerequisite_steps(args):
    steps = [
        PreFunctionStep(lambda: ensure_run_by_root(), u"Check if invoked by root user"),
        PreFunctionStep(lambda: ensure_correct_centos_version(args.skip_os_version_check),
                        u"Check if correct operating system version"),
        PreFunctionStep(lambda: ensure_good_path_environment(_logger), u"Configure PATH"),
        PreFunctionStep(lambda: ensure_config_dir_set(args.config_dir),
                        u"Configure configuration directory"),
        PreFunctionStep(lambda: ensure_not_installed(resolve_webapp_version()),
                        u"Check if software already installed"),
        PreFunctionStep(lambda: log_system_information_and_arguments(args), u"Log system information"),
        create_base_repo_available_step(REPO_STATE, _logger),
        create_hostname_resolvable_step(_logger),
        PreFunctionStep(lambda: socket_server_version_check(args.socket_server),
                        u"Determine IRIS Socket Server version"),
        make_verify_executable_is_callable_step(u"rsw-rpm-radarsw-repo-create", _logger)
    ]

    if not args.skip_geoserver_installation:
        steps.append(make_gis_dump_existence_check_step(args.gis_db_dump))
        if not args.skip_terrain:
            steps.append(make_terrain_dir_existence_check_step(args.terrain_dir))

    return steps


def create_installation_steps(args):
    steps = [
        BASE_REPO_SETUP_STEP,
        make_remove_postgres_92_packages_step(),
        ShellCommandStep(["rsw-rpm-radarsw-repo-create"], u"Install 'radarsw' Yum repo", logger=_logger),
        ShellCommandStep(install_packages_command(), u"Install RPM packages",
                         help_message=
                         """Couldn't install packages. If you encountered a problem with Yum repos being
                         offline, try disabling all repos that have 0 packages available or when you
                         encounter an error such as 'Cannot find a valid baseurl for repo: <repo-id>'.

                         You can list all enabled repos with command
                          - yum repolist enabled

                         You'll see the package count on the rightmost column. You can disable the repo
                         by calling
                          - yum-config-manager --disable <repo-id>
                         where <repo-id> is the left-most column in the previous listing.""", logger=_logger),
    ]

    app_installation_steps = [
        ShellCommandStep(["rsw-postgresql-94-configure"], "Configure PostgreSQL database server", logger=_logger),
        ShellCommandStep(["service", "postgresql-9.4", "start"], "Start up PostgreSQL database server", logger=_logger),
        ShellCommandStep(["rsw-db-tool", "create"], "Create database for IRIS Focus", logger=_logger),
        ShellCommandStep(configure_iris_host_command(args.socket_server), "Configure IRIS Socket Server host", logger=_logger),
        ShellCommandStep(["service", "vaisala-radarsw-webapp", "restart"],
                         "Restart IRIS Focus after socket server configuration", logger=_logger),
        FunctionStep(lambda: wait_until_code(200, "localhost:34080", "/health", "IRIS Focus"),
                     "Wait for IRIS Focus to start up after socket server configuration"),
        ShellCommandStep(["/usr/vaisala/radarsw/configuration/bin/configure-map",
                          "-u", args.wms_url, args.admin_user, args.admin_password],
                         "Configure IRIS Focus basemap WMS URL", logger=_logger),
        ShellCommandStep(["/usr/vaisala/radarsw/webapp-proxy/bin/generate-temp-ssl-cert"],
                         "Generate temporary self-signed SSL certificate", logger=_logger),
        ShellCommandStep(["service", "haproxy", "start"],
                         "Start up HTTP proxy, load balancer and SSL termination", logger=_logger)
        # ShellCommandStep(["rsw-data-manager-db-tool", "create"], "Create database for Data Manager", logger=_logger),
        # ShellCommandStep(["service", "vaisala-radarsw-data-manager", "start"], "Start up Data Manager", logger=_logger)
    ]


    steps = steps + app_installation_steps

    if not args.skip_geoserver_installation:
        steps = steps + create_basemap_installation_steps(args.gis_db_dump, args.max_zoom, args.terrain_dir)

    if not args.skip_geoserver_site_configuration and not args.skip_geoserver_installation:
        steps = steps + create_geoserver_site_config_steps(args.socket_server, args.geoserver_config_url)

    return steps


def create_post_installation_steps():
    return [
        ShellCommandStep(["service", "monit", "start"], "Restart service monitoring daemon monit", logger=_logger),
        BASE_REPO_CLEANUP_STEP,
        create_remove_yum_cache_step(_logger)
    ]


def ensure_not_installed(installed_version):
    if installed_version:
        log_and_exit(u"\nIRIS Focus already installed. Version: {}".format(installed_version), _logger)


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-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_arg_parser():
    parser = argparse.ArgumentParser('rsw-installer',
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    # Required arguments
    req_group = parser.add_argument_group("required arguments")
    req_group.add_argument("-s", dest="socket_server",
                           help="IRIS socket server.", required=True,
                           metavar="SOCKET SERVER HOST")
    repo_group = parser.add_mutually_exclusive_group(required=True)
    repo_group.add_argument("--online", dest="online", default=False, action="store_true",
                            help="Allow online CentOS base repository")
    repo_group.add_argument("--offline", dest="offline", default=False, action="store_true",
                            help="Disable online CentOS base repository and require a " +
                                 "file://-protocol CentOS base repository")
    terrain_group = parser.add_mutually_exclusive_group(required=True)
    terrain_group.add_argument("--terrain-dir", dest="terrain_dir", metavar="DIRECTORY",
                               # default="./gis_db_dump", # TODO: need a sane default
                               help="Terrain data directory.")
    terrain_group.add_argument("--skip-terrain", dest="skip_terrain", default=False, action="store_true")
    map_group = parser.add_mutually_exclusive_group(required=True)
    map_group.add_argument("--skip-geoserver-installation",
                           dest="skip_geoserver_installation",
                           default=False, action="store_true")
    map_group.add_argument("--gis-db-dump", dest="gis_db_dump", metavar="PATH",
                           # default="./gis_db_dump", # TODO: need a sane default
                           help="GIS database dump.")

    # Optional arguments
    opt_group = parser.add_argument_group("optional arguments")
    opt_group.add_argument("--admin-password", dest="admin_password", default="admin123", required=False)
    opt_group.add_argument("-d", "--debug", dest="debug", action="store_true", help="Enable debug messages")
    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.")
    opt_group.add_argument("-c", "--config-dir", dest="config_dir",
                           default=os.environ.get("VAISALA_RADARSW_CONFIG_DIR", '/etc/vaisala/radarsw/configuration'),
                           metavar="DIRECTORY")
    opt_group.add_argument("--admin-user", dest="admin_user", default="admin")
    opt_group.add_argument("-g", "--geoserver-config-url", metavar="URL", dest="geoserver_config_url",
                           help="GeoServer configuration endpoint.", default="http://localhost:34180/geoserver")
    opt_group.add_argument("-w", "--wms", dest="wms_url", help="Basemap WMS address.", default="/wms", metavar="URL")
    opt_group.add_argument("--skip-geoserver-site-configuration", dest="skip_geoserver_site_configuration",
                           default=False, action="store_true")
    opt_group.add_argument("--max-zoom", dest="max_zoom", type=int, help=argparse.SUPPRESS)
    opt_group.add_argument("--skip-os-version-check", dest="skip_os_version_check", default=False, action="store_true")

    return parser


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()
    log_file = setup_logging(args.debug, args.dry_run)

    global REPO_STATE
    REPO_STATE = RepoState(args.online)

    pre_steps = create_prerequisite_steps(args)
    steps = create_installation_steps(args)
    post_steps = create_post_installation_steps()

    status = u"FAILED"
    try:
        log_title(u"Pre-installation checks")
        StepExecutor(pre_steps, _logger, args.dry_run).execute()
        log_title(u"Installation")
        StepExecutor(steps, _logger, args.dry_run).execute()
        log_title(u"Post installation steps")
        StepExecutor(post_steps, _logger, args.dry_run).execute()
        status = u"SUCCESSFUL"
    finally:
        if args.dry_run:
            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__':
    try:
        main()
    finally:
        try:
            base_repo_cleanup()
        except:
            pass
