#!python
# -*- coding: utf-8 -*-

from __future__ import print_function
import os
import shutil
import tempfile
import sys
import shlex
import subprocess
import logging
import time
import getpass
from fcntl import fcntl, F_GETFL, F_SETFL
from os import O_NONBLOCK, read

import dotconfig
import yaml


logger = logging.getLogger(__name__)


GLOBAL_OPT_DEFAULTS = {
    'distgit_threads': 20,
    'rhpkg_clone_timeout': 900,
    'rhpkg_push_timeout': 1200
}


def global_opt_default_string():
    res = '\n'
    for k in GLOBAL_OPT_DEFAULTS.iterkeys():
        res += '  {}:\n'.format(k)
    return res


CLI_OPTS = {
    'data_path': {
        'env': 'DOOZER_DATA_PATH',
        'help': 'Git URL or File Path to build data',
        'cli': '--data-path'
    },
    'group': {
        'env': 'DOOZER_GROUP',
        'help': 'Sub-group directory or branch to pull build data'
    },
    'working_dir': {
        'env': 'DOOZER_WORKING_DIR',
        'help': 'Persistent working directory to use',
        'cli': '--working-dir'
    },
    'user': {
        'env': 'DOOZER_USER',
        'help': 'Username for running rhpkg / brew / tito',
        'cli': '--user'
    },
    'global_opts': {
        'help': 'Global option overrides that can only be set from settings.yaml',
        'default': global_opt_default_string()
    },
    'kerb_cache': {
        'env': 'DOOZER_KERB_CACHE',
        'help': 'Path to kerberos ticket cache. default: /tmp/krb5cc_${UID}'
    },
    'ssh_path': {
        'env': 'DOOZER_SSH_PATH',
        'help': 'Path to ssh directory. default: ~/.ssh/'
    },
    'gitconfig': {
        'env': 'DOOZER_GITCONFIG',
        'help': 'Path to .gitconfig. default: ~/.gitconfig'
    },
    'container_bin': {
        'env': 'DOOZER_CONTAINER_BIN',
        'help': 'Binary to use for running the doozer container. If blank will find podman or docker and use whichever is available.'
    },
    'image': {
        'env': 'DOOZER_IMAGE',
        'help': 'Pull spec of image to use if not default'
    }
}

CLI_ENV_VARS = {k: v['env'] for (k, v) in CLI_OPTS.iteritems() if 'env' in v}

CLI_CONFIG_TEMPLATE = '\n'.join(['#{}\n{}:{}\n'.format(v['help'], k, v['default'] if 'default' in v else '') for (k, v) in CLI_OPTS.iteritems()])

REMOVE_ARGS = [v['cli'] for v in CLI_OPTS.itervalues() if 'cli' in v]


def config_is_empty(path):
    with open(path, 'r') as f:
        cfg = f.read()
        return (cfg == CLI_CONFIG_TEMPLATE)


def cmd_gather(cmd, set_env=None, realtime=False):
    global logger
    """
    Runs a command and returns rc,stdout,stderr as a tuple.

    If called while the `Dir` context manager is in effect, guarantees that the
    process is executed in that directory, even if it is no longer the current
    directory of the process (i.e. it is thread-safe).

    :param cmd: The command and arguments to execute
    :param set_env: Dict of env vars to set for command (overriding existing)
    :param realtime: If True, output stdout and stderr in realtime instead of all at once.
    :return: (rc,stdout,stderr)
    """

    if not isinstance(cmd, list):
        cmd_list = shlex.split(cmd)
    else:
        cmd_list = cmd

    cwd = os.getcwd()
    cmd_info = '[cwd={}]: {}'.format(cwd, cmd_list)

    env = os.environ.copy()
    if set_env:
        cmd_info = '[env={}] {}'.format(set_env, cmd_info)
        env.update(set_env)
    logger.info("Executing:cmd_gather {}".format(cmd_info))
    try:
        proc = subprocess.Popen(
            cmd_list, cwd=cwd, env=env,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except OSError as exc:
        logger.error("Subprocess errored running:\n{}\nWith error:\n{}\nIs {} installed?".format(
            cmd_info, exc, cmd_list[0]
        ))
        return exc.errno, "", "See previous error description."

    try:
        if not realtime:
            out, err = proc.communicate()
            rc = proc.returncode
        else:
            out = ""
            err = ""

            # Many thanks to http://eyalarubas.com/python-subproc-nonblock.html
            # setup non-blocking read
            # set the O_NONBLOCK flag of proc.stdout file descriptor:
            flags = fcntl(proc.stdout, F_GETFL)  # get current proc.stdout flags
            fcntl(proc.stdout, F_SETFL, flags | O_NONBLOCK)
            # set the O_NONBLOCK flag of proc.stderr file descriptor:
            flags = fcntl(proc.stderr, F_GETFL)  # get current proc.stderr flags
            fcntl(proc.stderr, F_SETFL, flags | O_NONBLOCK)

            rc = None
            while rc is None:
                output = None
                try:
                    output = read(proc.stdout.fileno(), 256)
                    print(output, end='')
                    out += output
                except OSError:
                    pass

                error = None
                try:
                    error = read(proc.stderr.fileno(), 256)
                    print(error, end='')
                    out += error
                except OSError:
                    pass

                rc = proc.poll()
                time.sleep(0.0001)  # reduce busy-wait
    except KeyboardInterrupt:
        # Ctrl+C sent, forward to process
        if proc:
            proc.terminate()
        return 255, out, err

    logger.info(
        "Process {}: exited with: {}\nstdout>>{}<<\nstderr>>{}<<\n".
        format(cmd_info, rc, out, err))
    return rc, out, err


PULL_SPEC = 'docker-registry.engineering.redhat.com/aos-team-art/doozer:latest'


def check_arg(args, opt):
    res = None
    for i in range(len(args)):
        if args[i] == opt:
            if len(args) > (i + 1):
                return args[i + 1]
    return res


def clean_args(args):
    res = []
    i = 0
    while i < len(args):
        if args[i] in REMOVE_ARGS:
            i += 2
        else:
            res.append(args[i])
            i += 1
    return res


def main():
    doozer_args = sys.argv[1:]
    args = {}
    for k, v in CLI_OPTS.iteritems():
        if 'cli' in v:
            args[k] = check_arg(doozer_args, v['cli'])

    doozer_args = clean_args(doozer_args)

    cfg = dotconfig.Config('rundoozer', 'settings',
                           template=CLI_CONFIG_TEMPLATE,
                           envvars=CLI_ENV_VARS,
                           cli_args=args)

    if config_is_empty(cfg.full_path):
        msg = (
            "It appears you may be using Doozer for the first time.\n"
            "Be sure to setup Doozer using the user config file:\n"
            "{}\n"
        ).format(cfg.full_path)
        print(msg)

    config = cfg.to_dict()

    # figure out what binary to use for container run
    # if given in config, assume they know what they are doing
    container_bin = config.get('container_bin', None)
    if not container_bin:
        rc, out, err = cmd_gather('which podman')
        if rc == 0:
            container_bin = 'podman'
        else:
            rc, out, err = cmd_gather('which docker')
            if rc == 0:
                container_bin = 'docker'
            else:
                print('ERROR! No container binary found!')
                os.exit(1)

    if not config['working_dir']:
        print('ERROR! Must provide working directory via --working-dir or {}'.format(cfg.full_path))
        sys.exit(1)

    working_dir = os.path.expanduser(config['working_dir'])  # in case of ~
    working_dir = os.path.abspath(working_dir)  # in case relative

    gitconfig = config.get('gitconfig', None)
    if not gitconfig:
        gitconfig = os.path.expanduser('~/.gitconfig')

    # Per Luke's comments, should the .ssh dir be copied and then mounted?
    ssh = config.get('ssh_path', None)
    if not ssh:
        ssh = os.path.expanduser('~/.ssh')

    kerb = config.get('kerb_cache', None)
    if not kerb:
        kerb = '/tmp/krb5cc_{}'.format(os.getuid())
    kerb = os.path.expanduser(kerb)  # just in case

    user = config.get('user', None)
    if not user:
        user = getpass.getuser()

    if not os.path.isfile(kerb):
        msg = 'You have no kerberos ticket cache. Please run `{}`'
        if kerb.startswith('/tmp/krb5cc_'):
            msg = msg.format('kinit')
        else:
            msg = msg.format('kinit -c {}'.format(kerb))

        print(msg)
        exit(1)

    # create settings object to pass into container
    settings = {
        'data_path': config['data_path'],
        'group': config['group'],
        'user': user,
        'global_opts': config['global_opts']
    }

    # create working dir if not there
    if not os.path.isdir(working_dir):
        os.makedirs(working_dir)

    tmp_path = tempfile.mkdtemp()
    new_ssh = os.path.join(tmp_path, '.ssh')
    shutil.copytree(ssh, new_ssh)
    ssh = new_ssh

    # writing out manually to avoid importing pyyaml
    with open(os.path.join(working_dir, 'settings.yaml'), 'w') as f:
        yaml.safe_dump(settings, f, indent=2, default_flow_style=False)

    image = config.get('image', None)
    if not image:
        image = PULL_SPEC

    run_cmd = [container_bin, 'run', '--rm', '-it']
    run_cmd += ['-v', '{}:/kerb:z'.format(kerb)]
    run_cmd += ['-v', '{}:/working:z'.format(working_dir)]
    run_cmd += ['-v', '{}:/root/.ssh/'.format(ssh)]
    run_cmd += ['-v', '{}:/root/.gitconfig'.format(gitconfig)]
    run_cmd += [image]
    run_cmd += doozer_args

    print('Running: {}\n'.format(' '.join(run_cmd)))

    try:
        rc, std, err = cmd_gather(run_cmd, realtime=True)
        exit(rc)  # pass along return code
    except Exception, e:
        print('Failure running doozer container: ')
        print(e)
    finally:
        if os.path.isdir(tmp_path):
            shutil.rmtree(tmp_path)


if __name__ == '__main__':
    main()
