#!python

import sys
import os
import shutil
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.5.6"

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
    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)
    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. To get your token, visit https://api.cased.com/guard. Then run `cased configure <your-token>`."
        )
        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


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


def configure(user_token):

    # 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")

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

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


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
            )
        )
        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.")
            exit(0)
        else:
            output("URL has not been set.")
            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()
    output("Syncing with latest Guard server configuration.")
    _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.")
        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:
            if do_output:
                output("Failed to setup program: {}".format(program))

    if apps:
        if do_output and (local_shims_count != remote_shims_count):
            output(
                """New shims installed and updated. Please load a new shell for
           changes to take effect, and make sure cased-init has been eval'd."""
            )
    else:
        output("No guarded applications are configured on the Cased Guard server.")


def update():
    status = os.system(
        '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/cased/guard-install/main/install.sh)"'
    )

    if status != 0:
        print("Cased Guard failed to update. 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(
                "[error] The given program ({}) was not found on your PATH. Please specify the path to the program.".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)
    Client(program).execute(program_args, app_token, deny_if_unreachable, user_token)


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:
        help()

    token = args[2]
    configure(token)
    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]
    run(program, args[3:])

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()

    exit(0)

elif command == "verify":
    verify()

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

else:
    help()
    exit(0)
