#!/usr/bin/python3
"""This module provides package management functionality for MaRDI"""
# standard imports
import os
import sys
import time
import json
import itertools
import signal
import subprocess
import tempfile
import argparse
import concurrent.futures
from math import log10, floor
from time import strftime, localtime
from urllib import request
from urllib.error import HTTPError
from urllib.parse import urlparse, parse_qs, urlencode

# third party imports
import tomli
import tomli_w
import requests
import gi
from progress.bar import Bar
gi.require_version("OSTree", "1.0")
from gi.repository import OSTree, GLib

VERSION = '0.7'
BWRAP_DEFAULT = f"{os.getenv('HOME')}/.var/org.mardi.maps/deps/bubblewrap/_builddir/bwrap"
if os.getenv('BWRAP_CMD') is not None:
    BWRAP = str(os.getenv('BWRAP_CMD'))
else:
    BWRAP = BWRAP_DEFAULT
OSTREE_REPO_MODE_BARE_USER = 2
SPINNER = itertools.cycle(['-', '\\', '|', '/'])
HOME = os.getenv('HOME')
KEEP_FREE_SPACE = 3
VERBOSE = False
OG_SIGINT_HANLDER = signal.getsignal(signal.SIGINT)
TUSTARGET = "https://maps.math.rptu.de"
if os.getenv("MAPS_UPLOAD_SERVER") is not None:
    TUSTARGET = str(os.getenv("MAPS_UPLOAD_SERVER"))
TELETARGET = "https://maps.math.rptu.de"
if os.getenv("TELETARGET") is not None:
    TELETARGET = str(os.getenv("TELETARGET"))
AUTH = os.getenv("MTDAUTH")
TERM = os.getenv("SANDBOX_TERM")
TELECONSENT = False
MAPS_CONFIG = {}
COLORS = {
    "OKGREEN": '\033[92m',
    "WARNING": '\033[93m',
    "FAIL": '\033[91m',
    "ENDC": '\033[0m'
}
TUSPY_FOUND = False
try:
    from tusclient import client, exceptions
    from tusclient.storage import filestorage
    TUSPY_FOUND = True
except ModuleNotFoundError:
    print(f"{COLORS['WARNING']}Warning: Upload client not found! MaPS can still be used. However, "
          f"upload functionality is disabled.{COLORS['ENDC']}")


# Define a CLI
def addCLI():
    """Function adds a CLI to the package."""
    parser = argparse.ArgumentParser(
        prog='maps',
        description=("maps - MaRDI Packaging System : "
                     "Provides a unified interface for packaging "
                     "and deploying software environments."
                     ),
    )
    subparser = parser.add_subparsers(help="Use --help with each of the commands for more help ",
                                      dest="SubPars_NAME")
    # arguments for "main" path
    parser.add_argument('--version', action='version', version=VERSION)

    parser_runtime = subparser.add_parser("runtime",
                                          help="Command for deploying and executing runtimes")
    parser_runtime.add_argument('--command', dest='COMMAND', action='store',
                                default=False, help="Override for the command to run")
    parser_runtime.add_argument('-d', '--deploy', dest='DEPLOY', action='store',
                                default=False, help="deploy mode, for installing environments")
    parser_runtime.add_argument('-l', '--list', dest='LIST', action='store_true',
                                default=False, help="List all available environments")
    parser_runtime.add_argument('--list-local', dest='LIST_LOCAL', action='store_true',
                                default=False, help="List environments available offline")
    parser_runtime.add_argument('--repo', dest='REPO', help="Repository to use")
    parser_runtime.add_argument('--reset', dest='RESET', action='store',
                                default=False, help="Reset the runtime.")
    parser_runtime.add_argument('-r', '--run', dest='RUN', action='store',
                                default=False, help="Which runtime to play.")
    parser_runtime.add_argument('-u', '--uninstall', dest='UNINSTALL', action='store',
                                default=False, help="Uninstall a runtime")
    parser_runtime.add_argument('--update', dest="UPDATE", action='store',
                                default=False, help="Update a runtime")
    parser_runtime.add_argument('--url', dest="URL", action="store",
                                default=False, help="Deploy and launch a runtime from URL")
    parser_runtime.add_argument('-v', '--verbose', dest='VERBOSE', action='store_true',
                                help="enable verbose output")
    parser_runtime.add_argument('--no-gui', dest="NOGUI", action='store_true',
                                default=False, help="Disable GUI")
    parser_runtime.add_argument('--gui', dest="GUI", action='store_true',
                                default=False, help="Force enable GUI")
    parser_runtime.add_argument('-e', '--export-url', dest='EXPORTURL', action='store',
                                default=False, help="Export a runtime as a URL which maps can open")

    # arguments for remote management
    parser_remote = subparser.add_parser("remote",
                                         help="Command to add, delete, or list available remotes")
    parser_remote.add_argument('--add-remote', dest='REMOTE', nargs=2,
                               metavar=("REMOTE_NAME", "REMOTE_URL"), action='store',
                               default=False, help="Add REMOTE to local ostree repo")
    parser_remote.add_argument('--del-remote', dest="DEL_REMOTE", action='store',
                               default=False, help="Delete REMOTE from local ostree repo")
    parser_remote.add_argument('--list', dest="LIST", action='store_true',
                               default=False, help="List configured remotes")
    parser_remote.add_argument('--repo', dest='REPO', help="Repository to use")
    parser_remote.add_argument('-v', '--verbose', dest='VERBOSE', action='store_true',
                               help="enable verbose output")

    # arguments for packaging
    parser_pack = subparser.add_parser("package",
                                       help="Package mode, for creating runtimes")
    parser_pack.add_argument('-c', '--commit', dest='COMMIT', nargs=2, metavar=("TREE", "BRANCH"),
                             default=False, help="Commit TREE to BRANCH in REPO")
    parser_pack.add_argument('-i', '--initialize', dest='DIR',
                             help="initialize DIR with a good base tree")
    parser_pack.add_argument('-s', '--sandbox', dest='LOCATION',
                             help="Start a sandbox at LOCATION")
    parser_pack.add_argument('-v', '--verbose', dest='VERBOSE', action='store_true',
                             help="enable verbose output")
    parser_pack.add_argument('--repo', dest='REPO', help="Repository to use")
    parser_pack.add_argument('-u', '--upload', dest='UPLOAD', metavar="RUNTIME", action='store',
                             default=False, help="Upload RUNTIME for publishing.")
    parser_pack.add_argument('--no-gui', dest="NOGUI", action='store_true',
                             default=False, help="Disable GUI")
    parser_pack.add_argument('--gui', dest="GUI", action='store_true',
                             default=False, help="Force enable GUI")

    return parser, parser_runtime, parser_remote, parser_pack


def sanity_checks(parsers):
    """Some simple sanity checks, before the program proceeds"""
    if len(sys.argv) == 1:
        parsers[0].print_help()
        sys.exit(1)

    if len(sys.argv) == 2:
        if "runtime" in sys.argv:
            parsers[1].print_help()
        elif "remote" in sys.argv:
            parsers[2].print_help()
        elif "package" in sys.argv:
            parsers[3].print_help()
        sys.exit(1)


def create_config_file(config_path):
    """
    Function to create config file at config_path.
    """
    if VERBOSE:
        print("Creating MaPS config file...")
    if os.getenv("MAPS_NOTELE") is not None and os.getenv("MAPS_NOTELE") != "":
        telemetry_consent = 'n'
    elif (
        os.getenv("MAPS_TELEMETRY_CONSENT") is not None
        and os.getenv("MAPS_TELEMETRY_CONSENT") != ""
    ):
        telemetry_consent = 'y'
    else:
        telemessage = (
            "Would you like to enable anonymous telemtry about the runtimes you "
            "download? This enables MaPS to report some information about the "
            "runtime you are about to download to a telemtry server. The following "
            "information is collected:\n\t- Name of the remote repository"
            "\n\t- URL of the remote"
            "\n\t- Name of the runtime"
            "\n(Y/N)  > "
        )
        telemetry_consent = input(f"{telemessage}")
    consent = telemetry_consent.strip().lower() == 'y'
    if VERBOSE:
        if consent:
            print("Enabling telemetry...")
        else:
            print("Disabling telemetry...")
    with open(config_path, 'wb') as fo:
        config = {"Core": {"telemetry": consent}}
        tomli_w.dump(config, fo)
    return config


def program_init(repopath):
    """Init function verifies requirements, sets up the repo. Returns the OSTree Repo."""
    opt1 = "-q"
    opt2 = "1>/dev/null"
    opt3 = ""
    if VERBOSE:
        opt1 = ""
        opt2 = ""
        opt3 = "-v"
        print("Ensuring bubblewrap exists...")
    # step 1 : check bwrap is installed
    if (BWRAP == BWRAP_DEFAULT) and not os.path.isfile(BWRAP):
        print("Bubblewrap was not found, and is being automatically installed....")
        # bubblewrap directory exists
        # try cd and get fetch
        if os.path.isdir(BWRAP[0:-15]):
            if VERBOSE:
                print("Bubblewrap directory found. Refreshing...")
            subprocess.run(f"cd {BWRAP[0:-15]} && git fetch {opt1} --all --prune && git checkout "
                           f"{opt1} --force ak/sigint && git reset {opt1} --hard ak/sigint",
                           shell=True, check=True)
            if VERBOSE:
                print("Deleting _builddir...")
            subprocess.run(f"rm -rf {opt3} {BWRAP[0:-15]}/_builddir".split(), check=True)
        # bubblewrap directory does not exist
        # clone bubblewrap
        else:
            if VERBOSE:
                print("Cloning bubblewrap...")
            subprocess.run(f"git clone {opt1} https://github.com/aaruni96/bubblewrap.git "
                           f"{BWRAP[0:-15]}", shell=True, check=False)
            subprocess.run(f"cd {BWRAP[0:-15]} && git checkout {opt1} ak/sigint", shell=True,
                           check=True)
        # compile bwrap
        if VERBOSE:
            print("Compiling bubblewrap...")
        subprocess.run(f"cd {BWRAP[0:-15]} && meson _builddir {opt2} "
                       f"&& meson compile -C _builddir {opt2}", shell=True, check=True)
        print("Bubblewrap installed!")
    assert os.path.isfile(BWRAP)
    if VERBOSE:
        print("Bubblewrap okay!")
    # step 2 : create the directory
    if VERBOSE:
        print("Ensuring ostree repo directory exists...")
        opts = "-pv"
    else:
        opts = "-p"
    try:
        subprocess.run(f"mkdir {opts} {'/'.join(repopath.split('/'))}".split(), check=True)
    except subprocess.CalledProcessError:
        print(
            f"{COLORS['FAIL']}mkdir failed. This can happen if {COLORS['WARNING']}"
            + f"{repopath}{COLORS['FAIL']} is not a directory, for example, if it "
            + "is a regular file, a symlink, or some other type of file. Please check that "
            + f"{COLORS['WARNING']}{repopath}{COLORS['FAIL']} is either a "
            + f"directory, or does not exist, on a read-write filesystem.{COLORS['ENDC']}"
        )
        sys.exit(-1)    # lets decide, -1 is for all mkdir errors

    # step 3 : Configure a good known remote, if not already present
    repo = repopath.split('/')[-1]
    repopath = '/'.join(repopath.split('/')[0:-1])
    ostree_config_path = f"{repopath}/repo/config"
    ostree_config_exists = os.path.isfile(ostree_config_path)
    fd = os.open(repopath, os.O_RDONLY)
    repo = OSTree.Repo.create_at(fd, repo, OSTree.RepoMode(OSTREE_REPO_MODE_BARE_USER),
                                 GLib.Variant('a{sv}', {}), None)
    # if we just created a repo (and thus config), configure how we reserve free space
    if not ostree_config_exists:
        if VERBOSE:
            print("Just created repo, configuring free space parameters...")
        with open(ostree_config_path, 'a', encoding="utf-8") as fo:
            fo.write(f'min-free-space-size={KEEP_FREE_SPACE}GB\n')
        repo.reload_config()
    if (not repo.remote_list()) or "Official" not in repo.remote_list():
        if VERBOSE:
            print("Automatically adding official remote")
        repo.remote_add("Official", "https://repo.oscar-system.org/",
                        GLib.Variant('a{sv}', {"gpg-verify": GLib.Variant('b', False)}),
                        None)
    # step 4 : maps config
    global MAPS_CONFIG
    global TELECONSENT
    config_path = f"{repopath}/maps_config.toml"
    config_exists = os.path.isfile(config_path)
    # maps config doesnt exist, create it
    if not config_exists:
        if VERBOSE:
            print("MaPS config file not found!")
        MAPS_CONFIG = create_config_file(config_path)
        TELECONSENT = MAPS_CONFIG["Core"]["telemetry"]

    # maps config exists, read the telemetry consent info from it
    else:
        if VERBOSE:
            print("Config file found!")
        with open(config_path, 'rb') as rfo:
            MAPS_CONFIG = tomli.load(rfo)
        # check that the things we want exist
        if "Core" not in MAPS_CONFIG or "telemetry" not in MAPS_CONFIG["Core"]:
            if VERBOSE:
                print(COLORS["FAIL"]
                      + "Malformed config file! Printing for verification:"
                      + COLORS["ENDC"])
                print("---" + COLORS["WARNING"])
                print(tomli_w.dumps(MAPS_CONFIG).strip() + COLORS["ENDC"])
                print("---")
            MAPS_CONFIG = create_config_file(config_path)
            TELECONSENT = MAPS_CONFIG["Core"]['telemetry']
        else:
            if VERBOSE:
                print("Config file validated! Printing for verification:")
                print("---" + COLORS["OKGREEN"])
                print(tomli_w.dumps(MAPS_CONFIG).strip() + COLORS["ENDC"])
                print("---")
            TELECONSENT = MAPS_CONFIG["Core"]['telemetry']
    telemetry_consent = TELECONSENT
    # check if env vars override config file
    if (
        os.getenv("MAPS_TELEMETRY_CONSENT") is not None
        and os.getenv("MAPS_TELEMETRY_CONSENT") != ""
        and not TELECONSENT
    ):
        inputmessage = "MAPS_TELEMETRY_CONSENT is set, but telemetry was previously disabled!\n"\
            "Would you like to enable telemetry? (Y/N) > "
        telemetry_consent = input(inputmessage).strip().lower() == 'y'
        if telemetry_consent:
            if VERBOSE:
                print("Enabling telemetry...")
    if os.getenv("MAPS_NOTELE") is not None and os.getenv("MAPS_NOTELE") != "" and TELECONSENT:
        inputmessage = "MAPS_NOTELE is set, but telemetry consent was previously given!\nWould "\
            "you like to revoke telemetry consent? (Y/N) > "
        # confusing reuse of variable name "telemetry_consent"
        # in this case, telemetry_consent is True if the user inputs n
        # this makes the a common check possible to detect a mistmatch between env var and conf file
        telemetry_consent = input(inputmessage).strip().lower() == 'n'
        if telemetry_consent:
            if VERBOSE:
                print("Disabling telemetry...")
    # this is the common check the previous comment talks about
    if TELECONSENT != telemetry_consent:
        MAPS_CONFIG["Core"]["telemetry"] = telemetry_consent

    # write updated config to file
    with open(config_path, 'wb') as fo:
        tomli_w.dump(MAPS_CONFIG, fo)

    return repo


def make_remote_ref_list(repo, remote):
    """Given a repo and a remote, return a list of refs in the remote of that repo"""
    if remote is None:
        return []
    if repo.remote_list() is None:
        print(f"Repo {repo} has no remotes!")
        return []
    if repo.remote_list() is not None:
        if remote not in repo.remote_list():
            print(f"Repo {repo} has no remote {remote}!")
            return []
    remote_refs = []
    try:
        remote_refs.extend(list(repo.remote_list_refs(remote)[1].keys()))
    except GLib.Error as e:
        print(f"Error in make_remote_ref_list:\n\n\t{e}.\n\nCould not fetch remotes. "
              "Reporting local refs only. ")
        return []
    return remote_refs


def mode_list(repo):
    """Prints a list of available refs"""
    print("Available runtimes are :")
    refsino = repo.list_refs()
    refs = list(refsino[1].keys())
    if refs:
        print("Local")
        for ref in sorted(refs):
            commitref = repo.load_commit(refsino[1][ref])
            timestamp = OSTree.commit_get_timestamp(commitref[1])
            fmttime = strftime('%Y-%m-%d', localtime(timestamp))
            print(f"\t - {fmttime}\t{ref}")
    remotes = repo.remote_list()
    for remote in remotes:
        remote_refs = make_remote_ref_list(repo, remote)
        if remote_refs:
            print(remote)
            remote_url = repo.remote_get_url(remote)[1]
            try:
                r = requests.get(f"{remote_url}summary.json", timeout=5)
                if r.status_code != 200:
                    raise HTTPError(url=f"{remote_url}summary.json",
                                    code=r.status_code, msg=r.content.decode(),
                                    hdrs=r.headers, fp=None)
                summary = json.loads(r.content.decode())
                for ref in sorted(remote_refs):
                    if ref in summary:
                        print(f"\t - {summary[ref]}\t{ref}")
                    else:
                        print(f"\t - {ref}")
            except HTTPError:
                if VERBOSE:
                    print(f"{COLORS['WARNING']}Summary file not found on remote server. Printing "
                          f"list without dates!{COLORS['ENDC']}")
                for ref in sorted(remote_refs):
                    print(f"\t - {ref}")


def list_remotes(repo):
    """Returns a dict of known remotes and their URLs"""
    retval = {}
    for remote in repo.remote_list():
        retval[remote] = repo.remote_get_url(remote)[1]
    return retval


def mode_remotes(repo, args):
    """Administrative mode for remotes of the repo"""
    if args.LIST is not False:
        remotes = list_remotes(repo)
        for remote in remotes.items():
            print(f"{remote[0]}: {remote[1]}")
        return
    if args.REMOTE is not False:
        repo.remote_add(args.REMOTE[0], args.REMOTE[1],
                        GLib.Variant('a{sv}', {"gpg-verify": GLib.Variant('b', False)}), None)
        print(f"Added {args.REMOTE} to list of remotes!")
        return
    if args.DEL_REMOTE is not False:
        repo.remote_delete(args.DEL_REMOTE)
        print(f"Deleted {args.DEL_REMOTE} from list of remotes!")
        return


def disambiguate_runtime(repo: OSTree.Repo, rrstring: str, installed: bool = True):
    """Tries to disambiguate input into remote:runtime."""
    if ':' in rrstring:
        remote, runtime = rrstring.split(':')
        if VERBOSE:
            print(f"Remote explicitly specified as {remote}.")
        return remote, runtime

    # if not, check all repo / runtimes to find a unique set
    if VERBOSE:
        print("Remote not explicitly specified. Attempting to disambiguate...")
    found = False
    remote = ''
    runtime = ''

    if not installed:
        # installed should be false for things like depoly
        # so we search *everywhere*, even in the local repository
        # this is to address the edge case when something is fetched into the local repo
        # but not checked out to user filesystem yet
        installed_runtimes = repo.list_refs()[1].keys()
        for i in installed_runtimes:
            if rrstring == i.split(':')[-1]:
                if found:
                    print(f"Could not disambiguate {rrstring}!\nFound in at least two remotes "
                          f"{remote} and {i.split(':')[0]}, maybe more! Please spicfy your runtime "
                          f"in the form \n\n\tremote:runtime\n\nFor example\n\n\t{remote}:{runtime}"
                          "\n")
                    sys.exit(1)
                else:
                    found = True
                    if ':' in i:
                        remote, runtime = i.split(':')
                    else:
                        remote = '_local'
                        runtime = i

        if found:
            # found something with the same name installed, assume we want that, with a warning
            print(f"warning, possibly ambigious name. Assuming {remote}:{runtime}")
            return remote, runtime

        # all known remotes
        remotes = repo.remote_list()
        for sremote in remotes:
            remote_refs = make_remote_ref_list(repo, sremote)
            for ref in sorted(remote_refs):
                if rrstring == ref:
                    # found a target
                    if not found:
                        found = True
                        remote = sremote
                        runtime = rrstring
                    else:
                        print(f"Could not disambiguate {rrstring}!\nFound in at least two "
                              f"remotes {remote} and {sremote}, maybe more! Please specify your"
                              " runtime in the form \n\n\tremote:runtime\n\nFor example\n\n\t"
                              f"{remote}:{rrstring}\n")
                        sys.exit(1)
        if found:
            if VERBOSE:
                print(f"Disambiguated {rrstring} into {remote}:{runtime}!")
            return remote, runtime

        print(f"Unable to disambiguate {rrstring}! Returning garbage...")
        return ["notfound", "notfound"]
    # else installed is true
    # so we are disambiguating for run
    # search only in the installed runtimes
    # error if we don't find anything
    installed_runtimes = repo.list_refs()[1].keys()
    for i in installed_runtimes:
        if rrstring == i.split(':')[-1]:
            if found:
                print(f"Could not disambiguate {rrstring}!\nFound in at least two remotes "
                      f"{remote} and {i.split(':')[0]}, maybe more! Please spicfy your runtime "
                      f"in the form \n\n\tremote:runtime\n\nFor example\n\n\t{remote}:{runtime}"
                      "\n")
                sys.exit(1)
            else:
                found = True
                if ':' in i:
                    remote, runtime = i.split(':')
                else:
                    remote = "_local"
                    runtime = i
    if found:
        if VERBOSE:
            print(f"Disambiguated {rrstring} into {remote}:{runtime}!")
        return remote, runtime

    print(f"Unable to disambiguate {rrstring}! Returning garbage...")
    return ["notfound", "notfound"]


def make_bwrap_command(location, overlay, command, nogui, gui):
    """
    Subroutine builds the bubblewrap command to execute
    """
    senv = os.environ
    bwrapstring = f'{BWRAP} --forward-signals --unshare-user --unshare-pid --unshare-uts ' \
                  '--unshare-ipc --disable-userns --die-with-parent --uid 0 --gid 0 '
    if overlay:
        bwrapstring += f"--overlay-src {location}/rofs --overlay {location}/rwfs {location}/tmpfs /"
        bwrapstring += f" --bind {os.getenv('HOME')}/Public /home/runtime/Public"
    else:
        bwrapstring += f'--bind {location} /'
    bwrapstring += ' --proc /proc --dev /dev --ro-bind /sys /sys --tmpfs /tmp '
    try:
        session = senv['XDG_SESSION_TYPE']
    except KeyError:
        session = None
    if nogui:
        if VERBOSE:
            print("Disabling gui...")
    else:
        if session == 'wayland':
            rundir = senv["XDG_RUNTIME_DIR"]
            wayland_display = senv["WAYLAND_DISPLAY"]
            bwrapstring += f'--dir {rundir} --hostname runtime '
            if gui:
                bwrapstring += f'--ro-bind {rundir}/{wayland_display} {rundir}/{wayland_display} '
            else:
                bwrapstring += f'--ro-bind-try {rundir}/{wayland_display} '\
                    f'{rundir}/{wayland_display} '
        elif session == 'x11':
            bwrapstring += f'--ro-bind {senv["XAUTHORITY"]} /home/runtime/.Xauthority '
            if gui:
                bwrapstring += '--ro-bind /tmp/.X11-unix/ /tmp/.X11-unix/ '
            else:
                bwrapstring += '--ro-bind-try /tmp/.X11-unix/ /tmp/.X11-unix/ '
            senv['XAUTHORITY'] = '/home/runtime/.Xauthority'
            # Dirty hack to get the prompt right on debian-minimal
            senv['HOST'] = 'runtime'
            senv['HOSTNAME'] = 'runtime'
            senv['PS1'] = '${debian_chroot:+($debian_chroot)}\\u@runtime:\\w\\$ '
            senv['SUDO_USER'] = "."
            senv['SUDO_PS1'] = "."
        else:
            if gui:
                print("GUI was forcefully requested, but neither wayland nor X11 were found!")
                print("Please report this upstream!")
                sys.exit(-10)
            if VERBOSE:
                print("No graphical environment found! Continuing without graphics....")
    bwrapstring += command
    senv["HOME"] = "/home/runtime"
    senv["LC_ALL"] = "C"
    senv['USER'] = 'root'
    senv['SHELL'] = '/usr/bin/bash'
    senv['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:'\
                   '/usr/local/games'
    if TERM is not None:
        senv['TERM'] = TERM
    else:
        senv['TERM'] = 'xterm-256color'

    try:
        senv.pop("VIRTUAL_ENV")
    except KeyError:
        pass
    try:
        senv.pop("SESSION_MANAGER")
    except KeyError:
        pass
    try:
        senv.pop("XDG_SESSION_PATH")
    except KeyError:
        pass
    try:
        senv.pop("LOGNAME")
    except KeyError:
        pass
    try:
        senv.pop("SSH_AUTH_SOCK")
    except KeyError:
        pass
    try:
        senv.pop("XDG_GREETER_DATA_DIR")
    except KeyError:
        pass
    try:
        senv.pop("XDG_SEAT_PATH")
    except KeyError:
        pass
    try:
        senv.pop("GEM_HOME")
    except KeyError:
        pass
    try:
        senv.pop("MAIL")
    except KeyError:
        pass
    if VERBOSE:
        print("Bubblewrap command is")
        print(bwrapstring)
    return bwrapstring, senv


def mode_run(repo, args):
    """Function to execute a deployed environment"""

    remote, runtime = disambiguate_runtime(repo, args.RUN, installed=True)

    # check if the path exists
    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"
    if VERBOSE:
        print(f"Attempting to run {DATADIR}...")
    if not os.path.isdir(DATADIR):
        raise AssertionError(f"Data directory does not exist. Is {remote}:{runtime} installed"
                             " ?")

    # ensure share source and targets exist
    if VERBOSE:
        print("Making sure Public directories exist...")
        opts = '-pv'
    else:
        opts = '-p'
    try:
        subprocess.run(f"mkdir {opts} {os.getenv('HOME')}/Public".split(), check=True)
    except subprocess.CalledProcessError:
        print(
            f"{COLORS['FAIL']}mkdir failed. This can happen if {COLORS['WARNING']}"
            + f"{os.getenv('HOME')}/Public{COLORS['FAIL']} is not a directory, for example, if it "
            + "is a regular file, a symlink, or some other type of file. Please check that "
            + f"{COLORS['WARNING']}{os.getenv('HOME')}/Public{COLORS['FAIL']} is either a "
            + f"directory, or does not exist, on a read-write filesystem.{COLORS['ENDC']}"
        )
        sys.exit(-1)    # lets decide, -1 is for all mkdir errors
    if not os.path.isdir(f"{DATADIR}/rofs/home/runtime/Public"):
        try:
            subprocess.run(f"mkdir {opts} {DATADIR}/rwfs/home/runtime/Public".split(), check=True)
        except subprocess.CalledProcessError:
            print(
                f"{COLORS['FAIL']}mkdir failed. This can happen if {COLORS['WARNING']}"
                + f"{os.getenv('HOME')}/Public{COLORS['FAIL']} is not a directory, for example, if "
                + "it is a regular file, a symlink, or some other type of file. Please check that "
                + f"{COLORS['WARNING']}{os.getenv('HOME')}/Public{COLORS['FAIL']} is either a "
                + f"directory, or does not exist, on a read-write filesystem.{COLORS['ENDC']}"
            )
            sys.exit(-1)    # lets decide, -1 is for all mkdir errors

    # check for manifest file
    if os.path.isfile(f"{DATADIR}/rwfs/manifest.toml"):
        with open(f"{DATADIR}/rwfs/manifest.toml", 'rb') as manifest_file:
            command = tomli.load(manifest_file)
            command = command['Core']["command"]
    elif os.path.isfile(f"{DATADIR}/rofs/manifest.toml"):
        with open(f"{DATADIR}/rofs/manifest.toml", 'rb') as manifest_file:
            command = tomli.load(manifest_file)
            command = command['Core']["command"]
    else:
        command = "bash"
    if args.COMMAND:
        command = args.COMMAND
    if command == '':
        raise ValueError
    # launch sandbox
    print(f"Launching {remote}:{runtime}...")
    # construct bwrapstring
    bwrapstring, senv = make_bwrap_command(f"{DATADIR}", True, command, args.NOGUI, args.GUI)
    rstatus = subprocess.run(bwrapstring.split(),
                             env=senv, check=False)
    if rstatus.returncode != 0:
        print(f"Sandbox exited with return code {rstatus.returncode}")
    elif VERBOSE:
        print("Sandbox exited successfully!")


def transmit_telemetry(repo, remote: str, target: str):
    """
    Function to transmit telemetry about what runtime is about to be downloaded to server.
    This happens only if the user has opted into providing telemetry.
    """
    # strip password data from remote
    remotes = list_remotes(repo)
    # a remote URL is of the form
    # http://username:password@domain.tld/path
    # we just strip out the 'username:password' bit
    # (everything in front of '@', if it exists).
    o = urlparse(remotes[remote])
    newo = f"{o.scheme}://"
    newo += o.netloc.split('@')[-1] + o.path
    datadict = urlencode({"reponame": remote, "repourl": newo, "runtime": target}).encode()
    req = request.Request(f"{TELETARGET}/ping", data=datadict)
    try:
        with request.urlopen(req):
            pass
    except HTTPError as e:
        print("Error while transmitting telemetry:")
        print(e)


def download(repo, repopath, remote, target):
    """Function to download a repo from remote"""
    if TELECONSENT:
        transmit_telemetry(repo, remote, target)
    subprocess.run(f"ostree --repo={repopath} pull '{remote}:{target}'", check=True, shell=True)


# Uninstall
def uninstall_runtime(repo, args):
    """Function to remove a runtime from both the local disk checkout, and the local repo"""
    # Check if runtime is checked out
    FLAG_DIREXISTS = False
    FLAG_REFEXISTS = False
    remote, runtime = disambiguate_runtime(repo, args.UNINSTALL, installed=True)
    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"
    if VERBOSE:
        print(f"Trying to remove {remote}:{runtime}...")
    if os.path.isdir(DATADIR):
        FLAG_DIREXISTS = True
        if VERBOSE:
            print("Deleting files...")
            opts = '-rvf'
        else:
            opts = '-rf'
        subprocess.run(f"rm {opts} {DATADIR}".split(), check=True)

    for sruntime in repo.list_refs()[1].keys():
        if runtime in sruntime:
            FLAG_REFEXISTS = True
            if VERBOSE:
                print("Marking branch for deletion from repo...")
            repo.set_ref_immediate(remote, runtime, None, None)
            if VERBOSE:
                print("Pruning repo...")
            repo.prune(OSTree.RepoPruneFlags(2), -1, None)
            break

    if not (FLAG_DIREXISTS and FLAG_REFEXISTS):
        print(f"Error, {remote}:{runtime} isn't deployed and thus cannot be uninstalled!")
        sys.exit(2)
    else:
        print(f"Uninstalled {runtime} !")


def validate_runtime_name(runtime_id: str):
    """
    Function to validate that a given runtime ID conforms to naming rules
    """
    if VERBOSE:
        print("Validating runtime name...")
    # name must be split into name/platform/version
    # so, slash split string must have length 3
    assert len(runtime_id.split('/')) == 3, "Must have 3 parts in runtime identifier"
    if VERBOSE:
        print("Name has 3 parts separated with '/'!")

    # first part must be in rDNS. For now, it means that dot split string must have length
    # greater than or equal to 3. most commonly, it will be 3
    assert len(runtime_id.split('/')[0].split('.')) >= 3, "First part must be in reverse DNS format"
    if VERBOSE:
        print("First part is in rDNS!")

    # validate platform. for now, only x86_64 is supported
    assert runtime_id.split('/')[1] == 'x86_64'
    if VERBOSE:
        print(f"Platform is '{runtime_id.split('/')[1]}'!")
        print("Name validated!")


# Update
def mode_update(repo, repopath, args, remote=""):
    """Function to update a runtime identifier to its recent version (if any)"""
    if not args.UPDATE:
        args.UPDATE = args.DEPLOY
    # check if ref is installed
    if remote == "":
        remote, runtime = disambiguate_runtime(repo, args.UPDATE, installed=True)
    else:
        runtime = args.UPDATE.split(':')[-1]
    installed = runtime in [key.split(':')[-1] for key in repo.list_refs()[1].keys()]
    if VERBOSE:
        print(f"List of installed runtimes is {repo.list_refs()[1].keys()}")
    if not installed:
        print(f"{runtime} is not installed, hence cannot be updated! Try --deploy instead")
        return 1

    # check if we need an update
    same = repo.list_refs()[1][f'{remote}:{runtime}'] ==\
        repo.remote_list_refs(remote)[1][runtime]
    if VERBOSE:
        print(f"Local refhash = {repo.list_refs()[1][f'{remote}:{runtime}']}")
        print(f"Remote refhash = {repo.remote_list_refs(remote)[1][runtime]}")
    if same:
        print(f"{runtime} is already up to date, refreshing!")
    else:
        # download the update
        download(repo, repopath, remote, runtime)

    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"

    # clean out the data dir
    if VERBOSE:
        opts1 = "-rvf"
    else:
        opts1 = "-rf"
    subprocess.run(f"rm {opts1} {DATADIR}/rofs".split(), check=True)

    # checkout branch to tree
    refhash = repo.remote_list_refs(remote)[1][runtime]
    tfd = os.open(DATADIR, os.O_RDONLY)
    osopts = blank_options()
    osopts.bareuseronly_dirs = True
    osopts.mode = OSTree.RepoCheckoutMode(1)
    if VERBOSE:
        print(f"Checking out tree from repo to {DATADIR}/rofs ...")
    repo.checkout_at(osopts, tfd, "rofs", refhash, None)

    print(f"Success... {args.UPDATE} is now updated!")

    return 0


def checkout(repo, remote, runtime):
    """Function to checkout an already fetched runtime from the OSTREE repository to filesystem."""
    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"
    refhash = ""
    for sremote in repo.remote_list():
        if runtime in repo.remote_list_refs(sremote)[1]:
            refhash = repo.remote_list_refs(sremote)[1][runtime]
    if refhash == "":
        # refhash was not found in remote_list (obviously)
        refhash = repo.list_refs()[1][runtime]

    if VERBOSE:
        print("Setting up directories...")
        opts1 = '-pv'
    else:
        opts1 = '-p'
    try:
        subprocess.run(f"mkdir {opts1} {DATADIR}".split(), check=True)
        subprocess.run(f"mkdir {opts1} {DATADIR}/rwfs".split(), check=True)
        subprocess.run(f"mkdir {opts1} {DATADIR}/tmpfs".split(), check=True)
    except subprocess.CalledProcessError:
        print(
            f"{COLORS['FAIL']}mkdir failed. This can happen if {COLORS['WARNING']}"
            + f"{DATADIR}{COLORS['FAIL']} is not a directory, for example, if it "
            + "is a regular file, a symlink, or some other type of file. Please check that "
            + f"{COLORS['WARNING']}{DATADIR}{COLORS['FAIL']} is either a "
            + f"directory, or does not exist, on a read-write filesystem.{COLORS['ENDC']}"
        )
        sys.exit(-1)

    # checkout from local repo
    tfd = os.open(DATADIR, os.O_RDONLY)
    osopts = blank_options()
    osopts.bareuseronly_dirs = True
    osopts.mode = OSTree.RepoCheckoutMode(1)
    if VERBOSE:
        print(f"Checking out tree from repo to {DATADIR}/rofs ...")
    repo.checkout_at(osopts, tfd, "rofs", refhash, None)


# Deploy Mode
def mode_deploy(repo, repopath, args):
    """Function to deploy from repo to local disk"""

    remote, runtime = disambiguate_runtime(repo, args.DEPLOY, installed=False)

    # check that runtime exists in remote
    if runtime in make_remote_ref_list(repo, remote):
        pass
    elif runtime in list(repo.list_refs()[1].keys()):
        pass
    else:
        print("Error: runtime not found! Use list mode --list to view available runtimes.")
        sys.exit(1)

    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"

    # check if update
    if os.path.exists(DATADIR):
        print("Directory already exists, trying to update...")
        if VERBOSE:
            print(f"Data directory is {DATADIR}")
        ret = mode_update(repo, repopath, args, remote=remote)
        if ret == 1:
            raise AssertionError("Error: Unknown error!")
        return

    # download
    if remote != '_local':
        if VERBOSE:
            print(f"Downloading {remote}:{runtime}...")
        download(repo, repopath, remote, runtime)

    # setup directories
    checkout(repo, remote, runtime)
    print(f"Success... {remote}:{runtime} is now ready to use!")


def blank_options():
    """Return an OSTree.RepoCheckoutAtOptions object,
    with all (most) options blanked out explicitly """
    opts = OSTree.RepoCheckoutAtOptions()
    opts.bareuseronly_dirs = False
    # opts.devino_to_csum_cache =
    opts.enable_fsync = False
    opts.enable_uncompressed_cache = False
    # opts.filter =
    # opts.filter_user_data =
    opts.force_copy = False
    opts.force_copy_zerosized = False
    opts.mode = OSTree.RepoCheckoutMode(0)
    opts.no_copy_fallback = False
    opts.overwrite_mode = OSTree.RepoCheckoutOverwriteMode(0)
    opts.process_passthrough_whiteouts = False
    opts.process_whiteouts = False
    # opts.sepolicy
    opts.sepolicy_prefix = ''
    # opts.subpath = ''
    # opts.unused_bools = []
    # opts.unused_ints = []
    # opts.unused_ptrs = []
    return opts


# Package Mode
def mode_package(repo, repopath, args):
    """Function for package mode. Not intended to be used by "end users" """
    # --initialize
    if args.DIR is not None:
        refhash = ''
        if 'Official:base/x86_64/debian' not in list(repo.list_refs()[1].keys()):
            # import base to local repo
            if VERBOSE:
                print("base/x86_64/debian not found locally, fetching...")
            refhash = repo.remote_list_refs("Official")[1]['base/x86_64/debian']
            download(repo, repopath, "Official", "base/x86_64/debian")
        else:
            refhash = repo.list_refs()[1]['Official:base/x86_64/debian']
        with tempfile.TemporaryDirectory() as tmpdir:
            tfd = os.open(tmpdir, os.O_RDONLY)
            osopts = blank_options()
            osopts.bareuseronly_dirs = True
            osopts.mode = OSTree.RepoCheckoutMode(1)
            repo.checkout_at(osopts, tfd, "ostree", refhash, None)
            # we want an empty dir to work with
            # step1 check if dir exists
            if os.path.isdir(args.DIR):
                # step2 check if dir is empty
                if VERBOSE:
                    print(f"{args.DIR} exists..")
                if len(os.listdir(args.DIR)) == 0:
                    # dir is empty, we can continue
                    if VERBOSE:
                        print(f"{args.DIR} is empty. Initializing...")
                else:
                    print(f"{COLORS['FAIL']}Provided directory {args.DIR} is not empty...")
                    print(f"Can not continue!{COLORS['ENDC']}")
                    sys.exit(-2)
            else:
                if VERBOSE:
                    print("Creating directory...")
                    opts = '-v'
                else:
                    opts = ''
                if not os.system(f"mkdir {opts} {args.DIR}") == 0:
                    print(f"{COLORS['FAIL']}ERROR: Could not create {args.DIR}! Bailing!"
                          "{COLORS['ENDC']}")
                    sys.exit(-1)
            os.system(f"cp -r --reflink=auto {tmpdir}/ostree/* {args.DIR}/")
            print(f"Successfully initialized a base debian tree at {args.DIR} !")

    # --sandbox
    if args.LOCATION is not None:
        # location is a functional tree, we just have to sandbox in it
        # its the user's responsibility to ensure the tree is good
        print(f"Launching a sandbox in {args.LOCATION}...")
        bwrapstring, senv = make_bwrap_command(args.LOCATION, False, "/usr/bin/bash", args.NOGUI,
                                               args.GUI)
        rstatus = subprocess.run(bwrapstring.split(), env=senv, check=False)
        if VERBOSE:
            print("Exiting sandbox...")
        if rstatus.returncode != 0:
            print(f"Sandbox exited with return code {rstatus.returncode}")

    # --commit
    if args.COMMIT is not False:
        # we are given TREE and BRANCH. All we have to do is commit TREE to BRANCH
        # first validate that BRANCH follows a naming scheme we like
        validate_runtime_name(args.COMMIT[1])
        with concurrent.futures.ThreadPoolExecutor() as executor:
            future = executor.submit(commit, [repo, args.COMMIT[0], args.COMMIT[1]])
            print(f"Committing {args.COMMIT[0]} as {args.COMMIT[1]}. Please wait...")
            while True:
                sys.stdout.write(next(SPINNER))
                sys.stdout.flush()
                time.sleep(0.2)
                sys.stdout.write('\b')
                if future.done():
                    assert future.result() is None
                    sys.stdout.flush()
                    break
            print("Done!")
        _, refs = repo.list_refs()
        if VERBOSE:
            print("Currently available refs: ")
            print(list(refs.keys()))

    # --upload
    if args.UPLOAD:
        upload(repo, args.UPLOAD)


def commit(zarglist):
    """
    Function commits a tree to a repo in branch asynchronously,
    so spinner can be animated in the main thread to show activity.
    """
    repo = zarglist[0]
    tree = zarglist[1]
    branch = zarglist[2]
    if VERBOSE:
        print("\bPreparing transaction...")
    if tree[0] != '/':
        # if not an absolute pathname
        tree = f"./{tree}"
    repo.prepare_transaction()
    if VERBOSE:
        print("\bConstructing mutable tree in memory...")
    mutree = OSTree.MutableTree.new()
    if VERBOSE:
        print("\bFilling tree...")
    mfd = os.open('/'.join(tree.split('/')[0:-1]), os.O_RDONLY)
    repo.write_dfd_to_mtree(mfd, tree.split('/')[-1], mutree, None, None)
    mfile = repo.write_mtree(mutree, None)
    mcommit = repo.write_commit(None, None, None, None, mfile[1], None)
    if VERBOSE:
        print(f"\bCommitting to tree with hash {mcommit[1]}")
    repo.transaction_set_ref(None, branch, mcommit[1])
    repo.commit_transaction(None)


def reset(repo, runtime):
    """
    Function resets a runtime, simply by deleting the contents of the "rwfs" dir.
    """
    remote, runtime = disambiguate_runtime(repo, runtime, installed=True)
    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps/{remote}/{runtime}"
    if VERBOSE:
        print(f"Resetting {runtime}...")
        opts = '-rvf'
    else:
        opts = '-rf'
    subprocess.run(f"rm {opts} {DATADIR}/rwfs/*".split(), check=True)
    print(f"{runtime} reset successfully!")


def list_local(repo):
    """Function returns local refs only"""
    refs = list(repo.list_refs()[1].keys())
    if refs:
        return refs
    return []


def invalid_url_error():
    """
    Prints an error, and then waits for an output
    Repeated for most errors in the URL mode
    """
    print("The URL you have used is invalid. Please report this to the place you got your URL.")
    print("Press ENTER to continue...")
    input()
    sys.exit(-9)    # just a weird error code. I should probably get this more organized


# maps://runtime?run=Remote:runtime.name.tld/arch/version&Remote=https://user:password@blah.com/repo
# https://techpiezo.com/linux/create-a-custom-url-protocol-with-xdg-in-ubuntu/
def mode_url(repo, repopath, args):
    """
    URL mode, to deploy and execute runtimes from data given in a URL
    """
    o = urlparse(args.URL.replace("'", ""))

    # some very basic error handling
    # maps should only activate if o.scheme is maps!
    # in other words, the following check must never fail!
    # failing this will cause a crash without an error message
    assert o.scheme == "maps"

    # for now we only support runtime mode from URL
    # in fact, I can't think of a reason to support any other mode from the URL
    # given that we will automatically add a remote and fetch a runtime
    # if we come across a valid URL but missing remotes / runtimes
    if o.netloc != "runtime":
        invalid_url_error()

    q = parse_qs(o.query)
    # assert that required entries are available in the parsed query
    if "runtime" not in q:
        invalid_url_error()
    if "remotename" not in q:
        invalid_url_error()
    if "remoteurl" not in q:
        invalid_url_error()
    # check for optional entries in parsed query
    if "verbose" in q:
        # this only sets verbose *FROM THIS POINT ON*
        # not the best, but good enough
        global VERBOSE
        VERBOSE = True
    runtime = q["runtime"][0]
    remote_name = q["remotename"][0]
    remote_url = q["remoteurl"][0]

    # step 0: check if we know the remote repo
    check1 = False
    check2 = False
    check3 = False
    # step 0.1 : do we know the remote name?
    remotes = list_remotes(repo)
    check1 = remote_name in remotes

    if check1:
        # step 0.2 : does input remote URL match known remote URL?
        check2 = remote_url == remotes[remote_name]

    # step 0.3 : do we know the remote URL ?
    check3 = remote_url in list(remotes.values())
    if check3 and not check2:
        # we know the remote_url, but the remote_name is different
        # error out
        print(f"Remote {remote_name} not recognized, but the URL {remote_url} is known!")
        print("Known remotes are ")
        for i in remotes.items():
            print(f"{i[0]}: {i[1]}")
        invalid_url_error()

    if not check1 and not check3:
        # here check2 is always false
        assert not check2   # failing this assert will crash without an error
                            # this is fine because this is an impossible case ?
        # neither the name, nor the remote URL was known
        # add those to the local repo
        args.REMOTE = [remote_name, remote_url]
        mode_remotes(repo, args)
        check1 = True
        check2 = True
        check3 = True

    # step 1: check if runtime is already deployed
    isDeployed = f"{remote_name}:{runtime}" in list_local(repo)

    # step 2: deploy runtime if not deployed
    if not isDeployed and check1 and check2 and check3:
        print(f"Deploying {runtime}")
        args.DEPLOY = f"{remote_name}:{runtime}"
        mode_deploy(repo, repopath, args)
        isDeployed = True

    # step 3: run runtime
    if check1 and check2 and isDeployed:
        args.RUN = f"{remote_name}:{runtime}"
        mode_run(repo, args)


def mode_export_url(repo: OSTree.Repo, args: argparse.Namespace):
    """
    Subroutine exports a maps:// URL of the input remote and runtime
    """
    a = disambiguate_runtime(repo, args.EXPORTURL, False)
    remote_name = a[0]
    remote_url = list_remotes(repo)[a[0]]
    runtime = a[1]
    print("Generating a URL with the following info. Please check!")
    if '@' in remote_url:
        print(
            f"{COLORS['WARNING']} Warning: sharing a password protected remote, with the password. "
            + f"Please double check! {COLORS['ENDC']}"
        )
    print(f"- Remote name is\t{remote_name}")
    print(f"- Remote URL is\t\t{remote_url}")
    print(f"- Runtime is\t\t{runtime}")
    urlstring = (
        "maps://runtime?"
        + f"remotename={remote_name}&"
        + f"remoteurl={remote_url}&"
        + f"runtime={runtime}"
    )
    print(f"\n  {urlstring}\n")


# runtime mode: the default path for execution
def mode_runtime(repo, repopath, args):
    """
    Runtime mode, the default path for execution, and the "end user" mode.
    """
    if args.LIST:
        mode_list(repo)
    elif args.LIST_LOCAL:
        for ref in list_local(repo):
            print(ref)
    elif args.RESET:
        reset(repo, args.RESET)
    elif args.UNINSTALL:
        uninstall_runtime(repo, args)
    elif args.RUN:
        mode_run(repo, args)
    elif args.DEPLOY:
        mode_deploy(repo, repopath, args)
    elif args.UPDATE:
        mode_update(repo, repopath, args)
    elif args.URL:
        mode_url(repo, repopath, args)
    elif args.EXPORTURL:
        mode_export_url(repo, args)


def byteSI(inbytes):
    """
    Given bytes, turn it into K/M/G bytes. Only kind of accurate.
    """
    scale = {0: '', 1: 'K', 2: 'M', 3: 'G'}
    exp = log10(inbytes)
    exp = int(exp / 3)
    suf = scale[exp]
    rem = inbytes / (1024 ** exp)
    return f"{rem:.2f} {suf}"


def tus_upload(filename, storage_file, runtime):
    """
    tus upload subroutine
    """
    filesize = os.path.getsize(filename)
    chunksize = 1024 * 256  # chunksize in bytes
    headers = {'Authentication': f'Basic {AUTH}', 'Tus-Resumable': '1.0.0',
               'User-Agent': f'maps-{VERSION}'}
    upclient = client.TusClient(f'{TUSTARGET}/files/', headers=headers)
    storage = filestorage.FileStorage(storage_file)
    uploader = upclient.uploader(filename, store_url=True, url_storage=storage,
                                 metadata={'runtime': runtime, 'authentication': AUTH},
                                 chunk_size=chunksize)

    # going to need some try-catch here instead of just crashing
    # if auth or network fails
    print(f"Uploading {filename}")
    if not uploader.url:
        try:
            uploader.upload_chunk()
        except exceptions.TusCommunicationError as e:
            print("Upload failed!")
            if e.status_code is not None and e.response_content is not None:
                print(f"Error {e.status_code}: {e.response_content.decode()}")
            return False
        uploader.create_url()
        print("Created URL")

    offset = uploader.get_offset()
    uploader.offset = offset

    if VERBOSE:
        print(f"Current offset is {offset}")
        print(f"Total file size is {filesize}")
        print("=================")
    if offset == filesize:
        print("Upload already completed!")
        return 0

    progressBar = Bar("Uploading", max=floor(filesize / chunksize))
    progressBar.suffix = '%(percent).1d%%'
    progressBar.index = int(offset / chunksize)
    progressBar.start()
    progressBar.update()
    acc = 0
    while offset < filesize:
        told = time.time()
        uploader.upload_chunk()
        tnew = time.time()
        rate = chunksize / (tnew - told)
        if acc == 0:
            progressBar.suffix = f'%(percent).1d%% of {byteSI(filesize)}B | %(eta_td) s remaining' \
                                 f' | {byteSI(rate)}B/s'
        acc = acc + 1
        if acc == 5000:
            progressBar.suffix = f'%(percent).1d%% of {byteSI(filesize)}B | %(eta_td) s remaining' \
                                 f' | {byteSI(rate)}B/s'
            acc = 1
        offset = uploader.offset
        progressBar.next()
    progressBar.finish()

    return 0


def needs_tar(refhash, tarpath, datadir):
    """
    Given a repo and a runtime, function checks if a hash for that runtime already exists.
    If the hash already exists, it returns false (i.e, we don't need to tar the runtime).
    Otherwise, it returns true.
    """
    tardbpath = f"{datadir}/tardb.toml"
    if not os.path.isfile(tarpath):
        if VERBOSE:
            print("Tarfile doesn't already exist. Needs tar-ing!")
        return True
    if VERBOSE:
        print(f"Generting a hash for tarfile {tarpath}")
    tarhash = subprocess.check_output(["md5sum", tarpath]).decode().split()[0]
    if VERBOSE:
        print(f"Tar's hash is {tarhash}")
    if not os.path.isfile(tardbpath):
        # db didn't exist, clearly needs tar
        # also needs creating db
        if VERBOSE:
            print(f"Hash db not found! Creating at {tardbpath}")
            print(f"Writing hash to db for {tarpath}")
        with open(tardbpath, 'w', encoding="ascii") as tardbfile:
            tardbfile.write(f'"{refhash}"="{tarhash}"\n')
        return True
    with open(tardbpath, 'rb') as tardbfile:
        x = tomli.load(tardbfile)
        if refhash in x.keys():
            if x[refhash] == tarhash:
                if VERBOSE:
                    print("Hash found in db!")
                return False
    if VERBOSE:
        print("Hash not found in DB. We need to retar.")
    return True


def add_hash_to_db(refhash, tarpath, datadir):
    """
    Adds the md5hash of tarpath to tardb
    """
    tardbpath = f"{datadir}/tardb.toml"
    tarhash = subprocess.check_output(["md5sum", tarpath]).decode().split()[0]
    with open(tardbpath, 'a', encoding="ascii") as tardbfile:
        # assuming nobody has written a line without '\n' to the file
        tardbfile.write(f'"{refhash}"="{tarhash}"\n')


def upload(repo, runtime):
    """
    Given a local runtime, tar it and upload it. (Try)
    """
    if not TUSPY_FOUND:
        print(f"{COLORS['FAIL']}Error: Upload functionality is disabled because the package tuspy "
              "was not found.\n\n\tPlease install `tuspy` with pip, or `python3-tuspy` with your "
              f"distribution\'s package manager (if available)\n{COLORS['ENDC']}")
        return -2
    if AUTH is None:
        raise AssertionError("MTDAUTH not set! Cannot upload without authentication!")
    # just to be sure
    # second name validation right before upload
    validate_runtime_name(runtime)
    runtimes = list(repo.list_refs()[1].keys())
    if VERBOSE:
        print("Available local runtimes are:")
        for i in runtimes:
            print(i)
    if runtime not in runtimes:
        print("We only allow publishing locally made runtimes!")
        sys.exit(1)

    remote = "Local"
    DATADIR = f"{os.getenv('HOME')}/.var/org.mardi.maps"
    RUNTIMEDIR = f"{DATADIR}/{remote}/{runtime}"
    TARPATH = f"{DATADIR}/{remote}/{runtime}.tar.gz"
    STORAGEFILE = f"{DATADIR}/tustorage"
    REFHASH = repo.list_refs()[1][runtime]

    # if runtime is not checked out, do it
    if not os.path.isdir(RUNTIMEDIR):
        checkout(repo, remote, runtime)

    # if the refhash matches tar hash, don't re-tar
    if needs_tar(REFHASH, TARPATH, DATADIR):
        if VERBOSE:
            print("Making tarball...")
            opts = "-cv --use-compress-program=pigz -f"
        else:
            opts = "-c --use-compress-program=pigz -f"

        subprocess.run(f"tar {opts} {TARPATH} {RUNTIMEDIR}".split(), check=True)
        add_hash_to_db(REFHASH, TARPATH, DATADIR)

    # check storagefile
    good = 0
    if os.path.isfile(STORAGEFILE):
        with open(STORAGEFILE, encoding='UTF-8') as upfile:
            f = upfile.read()
            if f == ('', '\n'):
                good = 1
                if VERBOSE:
                    print("Local tus database empty!")
            else:
                try:
                    json.loads(f)
                except json.decoder.JSONDecodeError:
                    good = 2
                    if VERBOSE:
                        print("Local tus databse malformed!")
    if good == 1:
        # empty database, we can delete
        os.remove(STORAGEFILE)
    elif good == 2:
        os.rename(STORAGEFILE, f"{STORAGEFILE}.bak")

    if tus_upload(TARPATH, STORAGEFILE, runtime) != 0:
        print("something very bad happened")
        return -1
    return 0


# Main function
def main():
    """Main function"""
    # is modifying argv evil ?
    # if no "mode" is specified
    if ("runtime" not in sys.argv) and ("remote" not in sys.argv) and ("package" not in sys.argv):
        # if you're not just asking for help or version
        if "-h" in sys.argv:
            pass
        elif "--help" in sys.argv:
            pass
        elif "--version" in sys.argv:
            pass
        elif len(sys.argv) == 1:
            pass
        else:
            sys.argv.insert(1, "runtime")
    parsers = addCLI()
    parser = parsers[0]
    args = parser.parse_args()

    # Some sanity checks
    sanity_checks(parsers)
    global VERBOSE
    VERBOSE = args.VERBOSE

    # Setup
    if os.getenv('XDG_DATA_HOME') is not None:
        data = os.getenv('XDG_DATA_HOME')
    else:
        # this will crash if HOME is not set. How likely?
        data = f"{os.getenv('HOME')}/.local/share"
    data = f"{data}/org.mardi.maps"
    if args.REPO is None:
        repopath = f"{data}/ostree/repo"
    else:
        repopath = args.REPO
    repo = program_init(repopath)

    # Run mode
    if args.SubPars_NAME == 'runtime':
        mode_runtime(repo, repopath, args)
    elif args.SubPars_NAME == 'remote':
        mode_remotes(repo, args)
    elif args.SubPars_NAME == 'package':
        mode_package(repo, repopath, args)
    else:
        raise ValueError("Impossible case!")


if __name__ == "__main__":
    main()
