#!/usr/bin/env python
# -----------------------------------------------------------------------------
# HSS - Hermes Skill Server
# Copyright (c) 2020 - Patrick Fial
# -----------------------------------------------------------------------------
# hss_cli
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------

from __future__ import print_function
import sys
import os
import subprocess
import configparser
import shutil
import datetime
from appdirs import user_config_dir
import pkg_resources

from git import Repo

# ------------------------------------------------------------------------------
# globals
# ------------------------------------------------------------------------------

try:
    __version__ = pkg_resources.require("hss_server")[0].version
except Exception as e:
    __version__ = "0.0.0"

skills_directory = None
python_bin = None
ignored_files = ["__init__.py", "__pycache__", ".DS_Store"]

# ------------------------------------------------------------------------------
# eprint
# ------------------------------------------------------------------------------


def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

# ------------------------------------------------------------------------------
# error
# ------------------------------------------------------------------------------


def error(message):
    eprint(message)
    eprint("Sorry.")
    sys.exit(-1)

# ------------------------------------------------------------------------------
# parseArgs
# ------------------------------------------------------------------------------


def parseArgs():
    res = {}

    for i in range(len(sys.argv)):
        arg = sys.argv[i]

        if arg.startswith("--") or arg.startswith("-"):
            if i+1 < len(sys.argv) and not sys.argv[i+1].startswith("-"):
                res[arg.replace("-", "")] = sys.argv[i+1]
            else:
                res[arg.replace("-", "")] = None

    return res

# ------------------------------------------------------------------------------
# help
# ------------------------------------------------------------------------------


def help():
    version()

    print("\nUsage:")
    print("   $ ./hss-cli [-lhv][-iur arg]")
    print("\nOptions:")
    print(
        "\n   -l              List all installed skills.")
    print(
        "\n   -i [url]        Install a new skill using [url]. [url] must be a valid GIT link.")
    print(
        "   -u ([name])     Update an already installed skill named [name].")
    print("                   If [name] is ommited, ALL skills will be updated.")
    print(
        "\n   -r [name]       Uninstall an already installed skill named [name]")
    print("\n   -h, --help      Show this help and exit")
    print("   -v, --version   Show version and exit")
    print("\n")

# ------------------------------------------------------------------------------
# version
# ------------------------------------------------------------------------------


def version():
    print("Hermes Skill Server CLI v{}".format(__version__))

# ------------------------------------------------------------------------------
# run
# ------------------------------------------------------------------------------


def run():
    global skills_directory
    global python_bin

    config = configparser.ConfigParser()
    config_dir = args["config-dir"] if "config-dir" in args else user_config_dir("hss_server", "s710")
    config_ini = os.path.join(config_dir, "config.ini") if config_dir else None

    if not config_dir:
        raise Exception("Unable to get configuration dir. Use --config-dir option.")

    if not os.path.exists(config_ini):
        return error("Config file '{}' does not exist. Did you set up the skill-server?".format(config_ini))

    config.read(config_ini)

    if not "server" in config or not "skill_directory" in config["server"]:
        return error("Skills-directory not found in config file '{}'. Did you set up the skill-server?".format(config_ini))

    skills_directory = config["server"]["skill_directory"]

    python_bin = sys.executable

    if not python_bin or not os.path.exists(python_bin) or not os.path.isfile(python_bin):
        return error("No python3 binary found at '{}'".format(python_bin))

    if not skills_directory or not os.path.exists(skills_directory) or not os.path.isdir(skills_directory):
        return error("Invalid skills directory '{}'".format(skills_directory))

    if "list" in args or "l" in args:
        do_list()
    elif "install" in args or "i" in args:
        do_install(url=args["install"] if "install" in args else args["i"])
    elif "update" in args or "u" in args:
        do_update_all(name=args["update"] if "update" in args else args["u"])
    elif "uninstall" in args or "r" in args:
        do_uninstall(name=args["uninstall"] if "uninstall" in args else args["r"])
    else:
        help()

# ------------------------------------------------------------------------------
# do_list
# ------------------------------------------------------------------------------


def do_list():
    print("--------------------------------------------------------------------------------")
    print("{0: <35} {1: <8} {2: <7} {3: <20} {4: <1}".format("Skill", "Version", "Commit", "Date", "Config"))
    print("--------------------------------------------------------------------------------")

    for skill_name in os.listdir(skills_directory):
        if skill_name not in ignored_files:
            skill_directory = os.path.join(skills_directory, skill_name)
            config_ini = os.path.join(skill_directory, "config.ini")
            version_file = os.path.join(skill_directory, "version")
            repo = Repo(skill_directory)
            version = "-"

            cfg = "y" if os.path.exists(config_ini) else "n"

            if os.path.exists(version_file) and os.path.isfile(version_file):
                with open(version_file, 'r') as file:
                    version = file.read().replace("\n", "")

            try:
                headcommit = repo.head.commit
                commit = headcommit.hexsha[:7]
                dt = datetime.datetime.utcfromtimestamp(headcommit.committed_date).strftime("%m/%d/%Y, %H:%M:%S")

                print("{0: <35} {1: <8} {2: <7} {3: <20} {4: <1}".format(skill_name, version, commit, dt, cfg))
            except Exception as e:
                pass

# ------------------------------------------------------------------------------
# do_install
# ------------------------------------------------------------------------------


def do_install(url):
    if not url:
        return error("A valid GIT url must be given to install a skill")

    repo_name = os.path.basename(os.path.normpath(url))
    skill_directory = os.path.join(skills_directory, repo_name)
    venv_directory = os.path.join(skill_directory, "venv")
    config_ini = os.path.join(skill_directory, "config.ini")
    config_ini_default = os.path.join(skill_directory, "config.ini.default")
    venv_python = os.path.join(venv_directory, "bin", "python3")

    if os.path.exists(skill_directory):
        return error("Cannot install skill '{}', directory '{}' already exists".format(repo_name, skill_directory))

    print("Installing '{}' into '{}'".format(repo_name, skill_directory))

    # try to clone git repository first. assume skill-name equals repo name
    # e.g. https://github.com/patrickjane/hss-skill-s710-weather -> hss-skill-s710-weather

    print("Cloning repository ...")

    try:
        Repo.clone_from(url, skill_directory)
    except Exception as e:
        error("Failed to clone repo from '{}'".format(e))

    # setup python virtualenv and install dependencies
    # using subprocesses

    print("Creating venv ...")

    subprocess.run([python_bin, "-m", "venv", venv_directory], cwd=skill_directory)

    print("Installing dependencies ...")

    subprocess.run([venv_python, "-m", "pip",
                    "install", "-r", "requirements.txt"], cwd=skill_directory)

    # finally, check if there is a config.ini.default. if so,
    # it should be copied to config.ini, and empty parameters should be queried
    # from the user.

    if os.path.exists(config_ini_default) and os.path.isfile(config_ini_default):
        print("Initializing config.ini ...")

        config = configparser.ConfigParser()
        config.read(config_ini_default)

        for sect in config.sections():
            print("Section '{}'".format(sect))

            for key in list(config[sect].keys()):
                if not config[sect][key]:
                    val = input("Enter value for parameter '{}': ".format(key))
                    config[sect][key] = val

        # then write back the changes to the config.ini

        with open(config_ini, 'w') as file:
            config.write(file)

    print("\nSkill '{}' successfully installed.\n".format(repo_name))

# ------------------------------------------------------------------------------
# do_uninstall
# ------------------------------------------------------------------------------


def do_uninstall(name):
    if not name:
        return error("Skill name must be given in order to uninstall a skill")

    # just plain and silly remove the skill directory. nothing fancy here.

    skill_directory = os.path.join(skills_directory, name)

    if not os.path.exists(skill_directory) or not os.path.isdir(skill_directory):
        return error("Unknown/invalid skill '{}' given: '{}' is not a valid skill directory".format(name, skill_directory))

    print("Uninstalling skill '{}'".format(name))
    print("This will erase the directory '{}'".format(skill_directory))
    print("WARNING: The operation cannot be undone. Continue? (yes|NO) ")

    cont = input("")

    if cont != "yes" and cont != "Yes":
        print("Aborted.")
        sys.exit(0)

    print("Uninstalling ...")

    shutil.rmtree(skill_directory)

    print("\nSkill '{}' successfully uninstalled.\n".format(name))


# ------------------------------------------------------------------------------
# do_update_one
# ------------------------------------------------------------------------------

def do_update_one(name):
    if not name:
        return error("Skill name must be given in order to uninstall a skill")

    skill_directory = os.path.join(skills_directory, name)
    config_ini = os.path.join(skill_directory, "config.ini")
    config_ini_default = os.path.join(skill_directory, "config.ini.default")
    old_headcommit = None

    if not os.path.exists(skill_directory) or not os.path.isdir(skill_directory):
        return error("Unknown/invalid skill '{}' given: '{}' is not a valid skill directory".format(name, skill_directory))

    print("Updating skill '{}' ... ".format(name))

    # first, open existing config, if it exists

    existing_cfg = None
    config = configparser.ConfigParser()

    if os.path.exists(config_ini) and os.path.isfile(config_ini):
        config.read(config_ini)

    # git pull to get the updates

    try:
        repo = Repo(skill_directory)
        old_headcommit = repo.head.commit

        origin = repo.remotes['origin']
        origin.pull()
    except Exception as e:
        return error("Failed to pull changes from git ({})".format(e))

    new_headcommit = repo.head.commit

    # now evaluate the (potentially changed) config.ini.default and get the delta
    # prompt for new unknown values

    if os.path.exists(config_ini_default) and os.path.isfile(config_ini_default):
        new_config = configparser.ConfigParser()
        new_config.read(config_ini_default)

        for sect in new_config.sections():
            if not sect in config:
                config.add_section(sect)

            for key in list(new_config[sect].keys()):
                if not key in config[sect]:
                    if new_config[sect][key]:
                        config[sect][key] = new_config[sect][key]
                    else:
                        val = input(
                            "Enter value for new parameter '{}': ".format(key))
                        config[sect][key] = val

        # then write back the changes to the config.ini

        with open(config_ini, 'w') as file:
            config.write(file)

    if new_headcommit and old_headcommit and new_headcommit != old_headcommit:
        print("\nSkill '{}' successfully updated.\n".format(name))
        return True

    print("\nNo update for skill '{}' available.\n".format(name))
    return False


# ------------------------------------------------------------------------------
# do_update_all
# ------------------------------------------------------------------------------

def do_update_all(name):
    if name:
        return do_update_one(name)

    n_updates = 0
    n_nones = 0

    cont = input("Updating ALL skills. Continue? (YES|no) ")

    if cont and cont is not "YES" and cont is not "yes":
        return

    for filename in os.listdir(skills_directory):
        if filename not in ignored_files:
            if do_update_one(filename):
                n_updates = n_updates + 1
            else:
                n_nones = n_nones + 1

    print("{} Skills updated, {} skills without update".format(n_updates, n_nones))

# ------------------------------------------------------------------------------
# main
# ------------------------------------------------------------------------------


if __name__ == "__main__":
    args = parseArgs()

    if args is None or "help" in args or "h" in args:
        help()
    elif "version" in args or "v" in args:
        version()
    else:
        run()
