#!python

import sys
import os
import shutil
import time
import json
import signal
import requests
import sys
import hashlib
from os.path import expanduser, exists

from pkg_resources import parse_version
from packaging import version


from cguard.core import Client
from cguard.requestor import GuardRequestor
from cguard.util import (
    home_dir,
    cguard_dir,
    shims_dir,
    read_settings,
    log_level,
    environment,
    autosync,
    output,
    debug,
)


VERSION = "0.6.3"

DEFAULT_URL = "https://api.cased.com"

help_docs = """Usage: cased <command> [<command args>]

Load cased automatically by appending the following to your ~/.bashrc or ~/.zshrc:

    eval "$(cased-init -)"

Common commands:
    configure [<user credentials>]                   Configure cased with user credentials. Run without an arg for web connect.
    sync                                             Sync all shims and settings with remote Cased Guard config
    update                                           Update the Cased Guard client with the latest version
    url [<remote url>]                               Get/set the remote url for the Cased server (default: http://localhost:3000)
    verify                                           Run diagnostics on cased, confirming things are setup correctly
    version                                          Output the cased version

Internal commands:
    local-shims                                      List all local cased shims
    remote-shims                                     List all remote shims
"""


shim_text = """#!/bin/sh

APP_TOKEN={0} DENY_IF_UNREACHABLE={1} cased run {2} "$@"
"""


def help():
    print(help_docs)
    sys.exit(0)


#### HELPER FUNCTIONS ########################


def _make_shim(program, token, program_path, deny_if_unreachable):
    deny_state = "0"
    if deny_if_unreachable:
        deny_state = "1"

    text = shim_text.format(token, deny_state, program_path)
    filepath = shims_dir() + "/" + program

    with open(filepath, "w+") as text_file:
        text_file.write(text)

    os.chmod(filepath, 509)


def _check_credentials():
    filepath = cguard_dir() + "/" + "credentials"
    return exists(filepath)


def _check_settings():
    filepath = cguard_dir() + "/" + "settings"
    return exists(filepath)


def _confirm_credentials():
    if not _check_credentials():
        output("Please configure your user token: run `cased configure`.")
        sys.exit(1)


def _get_credentials():
    _confirm_credentials()
    filepath = cguard_dir() + "/" + "credentials"
    with open(filepath, "r") as text_file:
        creds = text_file.read()

    return creds


def _check_for_new_version():
    try:
        url = "https://pypi.org/pypi/cased/json"
        versions = requests.get(url, timeout=20).json()
        releases = versions.get("releases")
        version_numbers = sorted(releases, key=parse_version, reverse=True)
        latest_version = version_numbers[0]

        if version.parse(latest_version) > version.parse(VERSION):
            output(
                "There is a newer version of Cased Guard available. Please update your client by running: `cased update`"
            )
        else:
            pass
    except:
        pass


def _setup_directories():
    # make necessary directories just to be sure
    cguard_d = cguard_dir()
    shims_d = shims_dir()

    if not os.path.exists(cguard_d):
        os.makedirs(cguard_d)
        output("Created guard directory")

    if not os.path.exists(shims_d):
        os.makedirs(shims_d)
        output("Created guard shims directory")


#### COMMON COMMANDS ########################


def configure():
    _setup_directories()

    filepath = cguard_dir() + "/credentials"

    # get user token
    requestor = GuardRequestor()
    res = requestor.identify_user()
    if res.status_code == 201:
        data = res.json()
        code = data.get("code")
        url = data.get("url")

        # Wait for 200, then timeout
        wait_text = """To continue, confirm your Cased account or sign up. Please visit:
{0}""".format(
            url
        )

        print(wait_text)

        while True:
            # poll the API for confirmation of connection
            res = requestor.check_for_identification(code)
            if res.status_code == 200:
                msg = "✅ Connected with Cased."
                output(msg)

                data = res.json()
                user_token = data.get("user").get("id")

                with open(filepath, "w+") as text_file:
                    text_file.write(user_token)
                    output("Credentials set and stored.")

                time.sleep(2)
                sync()

                break

            time.sleep(2)
    else:
        output("Failed to configure (bad response to identify user")
        sys.exit(1)


def configure_with_token(user_token):
    _setup_directories()

    filepath = cguard_dir() + "/credentials"
    with open(filepath, "w+") as text_file:
        text_file.write(user_token)

    output("Credentials set and stored.")
    sync()


def reset_configure():
    filepath = cguard_dir() + "/credentials"
    if os.path.exists(filepath):
        os.remove(filepath)
        output("Configuration has been reset.")


def get_remote_url():
    filepath = cguard_dir() + "/remote"
    if not exists(filepath):
        output(
            "Custom URL has not been set. Guard will use the default url: {}".format(
                DEFAULT_URL
            )
        )
        sys.exit(0)

    with open(filepath, "r") as text_file:
        url = text_file.read()

    print(url)


def set_remote_url(remote_url):
    filepath = cguard_dir() + "/remote"

    if remote_url == "--reset":
        if os.path.exists(filepath):
            os.remove(filepath)
            output("URL has been reset. Default will be used.")
            sys.exit(0)
        else:
            output("URL has not been set.")
            sys.exit(0)

    with open(filepath, "w+") as text_file:
        text_file.write(remote_url)

    output("Remote url set.")
    get_remote_url()


def sync(do_output=True):
    _confirm_credentials()
    _check_for_new_version()

    user_token = _get_credentials()
    requestor = GuardRequestor()

    filepath = cguard_dir() + "/settings"
    settings = {
        "poll": 1,
        "log_level": "info",
        "environment": "local",
        "autosync": True,
    }

    with open(filepath, "w+") as text_file:
        json.dump(settings, text_file)

    # Get applications
    res = requestor.get_applications(user_token, environment())
    if res.status_code != 200:
        output(
            "Error requesting current applications, unable to sync. Error: {}".format(
                str(res.status_code)
            )
        )
        sys.exit(1)

    if log_level() == "debug":
        debug(res.text)

    apps = res.json()

    # Remove all existing shims
    target = shims_dir()
    shims = sorted(os.listdir(target))

    # get the count of shims
    local_shims_count = len(shims)
    remote_shims_count = len(apps)  # todo: compare names later

    for shim in shims:
        uninstall_program(shim)

    # Install all apps
    for app in apps:
        program = app.get("program")
        app_id = app.get("id")
        settings = app.get("settings")

        deny_if_unreachable = settings.get(
            "deny_on_unreachable"
        )  # todo: change name server-side

        try:
            install_program(program, app_id, deny_if_unreachable, do_output)
        except:
            pass

    if apps:
        if do_output and (local_shims_count != remote_shims_count):
            output("New shims installed and updated.")
    else:
        output("No guarded applications are configured on the Cased Guard server.")


def update():
    status = os.system(
        "brew upgrade cased/tap/cased"
    )  # todo: make the manner of the update option more configurable

    if status != 0:
        print(
            "Cased Guard failed to update automatically with Homebrew. Please update manually."
        )
    else:
        print("Cased Guard has been updated.")


def verify():
    shim_on_path = False
    cguard_on_path = False
    cguard_init_on_path = False
    credentials_set = False
    settings_set = False

    path = os.environ.get("PATH")

    # confirm that the shims directory is on the PATH
    if shims_dir() in path:
        shim_on_path = True

    # confirm cguard is on the PATH
    cguard_filepath = shutil.which("cased")
    if cguard_filepath:
        cguard_on_path = True

    # confirm cguard-init is on the PATH
    cguard_init_filepath = shutil.which("cased-init")
    if cguard_init_filepath:
        cguard_init_on_path = True

    # confirm credentials are set
    if _check_credentials():
        credentials_set = True

    # confirm general settings
    if _check_settings():
        settings_set = True

    verifications = [
        {"cased on PATH                  ": cguard_on_path},
        {"cased-init on PATH             ": cguard_init_on_path},
        {"general settings are configured": settings_set},
        {"shim directory on PATH         ": shim_on_path},
        {"user credentials are configured": credentials_set},
    ]

    print("")
    for verification in verifications:
        for v, s in verification.items():
            if s == True:
                print("✅ {}    verified".format(v))
            else:
                print("❌ {}    not verified".format(v))
    print("")


#### INTERNAL COMMANDS ########################


def install_program(program, token, deny_if_unreachable, do_output=False):
    shim_path = shutil.which(program)
    if not shim_path:
        if do_output:
            output(
                "{} was not found on your PATH. Could not install shim".format(program)
            )

    if shims_dir() in shim_path:
        output(
            "Shim already exists."
        )  # todo: we can overwrite the existing, but it will require an explicit path (`which` won't work anymore since we have the shim!)

    details = "Installed Cased Guard shim for {}".format(program)

    _make_shim(program, token, shim_path, deny_if_unreachable)
    if do_output:
        output(details)


def list_local_shims():
    target = shims_dir()
    shims = sorted(os.listdir(target))
    for shim in shims:
        print(shim)


def list_remote_shims():
    _confirm_credentials()

    user_token = _get_credentials()
    requestor = GuardRequestor()
    res = requestor.get_applications(user_token, environment())
    apps = res.json()

    for app in apps:
        print(app.get("name"))


def run(program, program_args=[]):
    _confirm_credentials()

    if autosync() == True:
        # Always run sync to keep guard up to to date, but run silently.
        sync(False)

    app_token = os.environ.get("APP_TOKEN")
    deny_if_unreachable = os.environ.get("DENY_IF_UNREACHABLE")

    user_token = _get_credentials()
    program_args = " ".join(program_args)
    retval = Client(program).execute(
        program_args, app_token, deny_if_unreachable, user_token
    )
    return retval


def uninstall_program(program):
    target = shims_dir() + "/" + program
    if os.path.exists(target):
        os.remove(target)
    else:
        print("There is no shim for the program: {}".format(program))
        return


### CASED GUARD CLI
args = sys.argv
if len(args) == 1:
    help()

command = sys.argv[1]

if command == "configure":
    if not len(args) > 2:
        configure()
        sys.exit(0)

    arg2 = args[2]
    if arg2 == "--reset":
        reset_configure()
        sys.exit(0)
    else:
        token = arg2

    configure_with_token(token)
    sys.exit(0)

if command == "guard":
    path = None
    if not len(args) > 3:
        help()

    program = args[2]
    app_id = args[3]

    if len(args) > 4:
        path = args[3]

    install_program(program, app_id, True, path)


elif command == "local-shims":
    list_local_shims()


elif command == "remote-shims":
    list_remote_shims()


elif command == "run":
    if not len(args) > 2:
        help()

    program = args[2]
    retval = run(program, args[3:])
    sys.exit(retval)

elif command == "sync":
    sync()

elif command == "update":
    update()

elif command == "uninstall":
    if not len(args) > 2:
        help()

    program = args[2]
    uninstall_program(program)

elif command == "url":
    if len(args) > 2:
        url = args[2]
        set_remote_url(url)
    else:
        get_remote_url()

    sys.exit(0)

elif command == "verify":
    verify()

elif command == "version":
    print(VERSION)

else:
    help()
    exit(0)
