#!/usr/bin/env python
import datetime
from distutils.dir_util import copy_tree
import json
import os
from pathlib import Path
import requests
import subprocess
import sys
import time
from typing import Optional
import yaml

from tools.check_environment import dependencies_ok

sys.path.insert(0, "skyportal")


def initialize_submodules():
    """Initialize submodules if either submodule directory is empty"""
    do_initialize = any(
        len(list(Path(submodule).glob("*"))) == 0
        for submodule in ("kowalski", "skyportal")
    )
    if do_initialize:
        p = subprocess.run(["git", "submodule", "update", "--init", "--recursive"])
        if p.returncode != 0:
            raise RuntimeError("Failed to initialize fritz's submodules")


def patch_skyportal():
    """Make fritz-specific file modifications to SkyPortal."""
    print("Applying fritz-specific patches to SkyPortal")

    p = subprocess.run(["git", "rev-parse", "--short", "HEAD"], stdout=subprocess.PIPE)
    git_hash = p.stdout.decode("utf-8").strip()

    # add Fritz-specific SP extensions
    copy_tree("extensions/skyportal/", "skyportal/")

    # Add fritz and SkyPortal git version to skyportal/__init__.py
    from skyportal import __version__

    init_file = "skyportal/skyportal/__init__.py"
    with open(init_file, "r") as f:
        init = f.readlines()
    out = []
    for line in init:
        if line.startswith("__version__ = "):
            __version__ = "+".join(
                [v for v in __version__.split("+") if not v.startswith("fritz")]
            )
            line = f'__version__ = "{__version__}+fritz.{git_hash}"\n'
        out.append(line)
    with open(init_file, "wb") as f:
        f.write("".join(out).encode("utf-8"))

    # get the git log for SP and add it to the SP distro
    p = subprocess.run(
        [
            "git",
            "--git-dir=.git",
            "log",
            "--no-merges",
            "--pretty=format:[%cI %h %cE] %s",
            "-1000",
        ],
        capture_output=True,
        universal_newlines=True,
        cwd="skyportal",
    )
    out = p.stdout
    with open("skyportal/data/gitlog.txt", "w") as spgl:
        spgl.write(out)

    # add Fritz-specific dependencies for SP
    # js
    with open("extensions/skyportal/package.fritz.json", "r") as f:
        fritz_pkg = json.load(f)
    with open("skyportal/package.json", "r") as f:
        skyportal_pkg = json.load(f)

    skyportal_pkg["dependencies"] = {
        **skyportal_pkg["dependencies"],
        **fritz_pkg["dependencies"],
    }
    with open("skyportal/package.json", "w") as f:
        json.dump(skyportal_pkg, f, indent=2)

    # python
    with open(".requirements/ext.txt", "r") as f:
        ext_req = f.readlines()
    with open("skyportal/requirements.txt", "r") as f:
        skyportal_req = f.readlines()
    with open("skyportal/requirements.txt", "w") as f:
        f.writelines(skyportal_req)
        for line in ext_req:
            if line not in skyportal_req:
                f.write(line)

    # The skyportal .dockerignore includes config.yaml, but we would like
    # to add that file; we therefore overwrite the .dockerignore
    with open("skyportal/.dockerignore", "w") as f:
        f.write(".*\n")


def generate_kowalski_token(
    user_id: str,
    jwt_secret: str,
    jwt_algorithm: str = "HS256",
    jwt_exp_delta_seconds: Optional[int] = None,
):
    """
    Generate a token for SkyPortal to access Kowalski
    """
    import jwt

    jwt_config = {
        "user_id": user_id,
        "JWT_SECRET": jwt_secret,
        "JWT_ALGORITHM": jwt_algorithm,
        "JWT_EXP_DELTA_SECONDS": jwt_exp_delta_seconds,
    }

    payload = {"user_id": jwt_config["user_id"]}
    if jwt_config["JWT_EXP_DELTA_SECONDS"]:
        payload["exp"] = datetime.datetime.utcnow() + datetime.timedelta(
            seconds=jwt_config["JWT_EXP_DELTA_SECONDS"]
        )
    jwt_token = jwt.encode(
        payload, jwt_config["JWT_SECRET"], jwt_config["JWT_ALGORITHM"]
    )

    return jwt_token


def check_config_exists(cfg="fritz.defaults.yaml", yes=False):
    c = cfg.replace(".defaults", "")
    if not Path(c).exists():
        cd = (
            input(
                f"{c} does not exist, do you want to use {cfg} (not recommended)? [y/N] "
            )
            if not yes
            else "y"
        )
        if cd.lower() == "y":
            subprocess.run(["cp", f"{cfg}", f"{c}"], check=True)
        else:
            raise IOError(f"{c} does not exist, aborting")


def check_config(cfg="fritz.defaults.yaml", yes=False):
    """
    Check if config exists, generate a K token for SP, adjust cfg and distribute to K and SP
    """
    c = cfg.replace(".defaults", "")
    check_config_exists(cfg=cfg, yes=yes)

    # generate a token for SkyPortal to talk to Kowalski
    with open(c) as config_yaml:
        config = yaml.load(config_yaml, Loader=yaml.FullLoader)

    kowalski_token = generate_kowalski_token(
        user_id=config["kowalski"]["server"]["admin_username"],
        jwt_secret=config["kowalski"]["server"]["JWT_SECRET_KEY"],
        jwt_algorithm=config["kowalski"]["server"]["JWT_ALGORITHM"],
        jwt_exp_delta_seconds=config["kowalski"]["server"]["JWT_EXP_DELTA_SECONDS"],
    )

    config["skyportal"]["app"]["kowalski"]["token"] = kowalski_token

    # get Docker gateway IP
    command = [
        "docker",
        "network",
        "inspect",
        "bridge",
        '--format={{index .IPAM.Config 0 "Gateway"}}',
    ]
    p = subprocess.run(command, stdout=subprocess.PIPE)
    gateway_ip = p.stdout.decode("utf-8").strip()
    config["kowalski"]["skyportal"]["host"] = gateway_ip
    config["skyportal"]["app"]["kowalski"]["host"] = gateway_ip

    # strip down, adjust, and copy over to Kowalski and SkyPortal
    config_kowalski = {"kowalski": config["kowalski"]}  # don't need the SP stuff
    with open("kowalski/config.yaml", "w") as kowalski_config_yaml:
        yaml.dump(config_kowalski, kowalski_config_yaml)

    # Docker-specific SkyPortal stuff:
    config["skyportal"]["database"]["host"] = "db"
    config["skyportal"]["server"]["url"] = "http://localhost:5000"
    config_skyportal = config["skyportal"]  # don't need the K stuff
    with open("skyportal/config.yaml", "w") as skyportal_config_yaml:
        yaml.dump(config_skyportal, skyportal_config_yaml)

    # update fritz.yaml:
    with open(c, "w") as config_yaml:
        yaml.dump(config, config_yaml)


def skyportal_api(method, endpoint, token, data=None):
    headers = {"Authorization": f"token {token}"}
    response = requests.request(method, endpoint, json=data, headers=headers)
    return response


def get_skyportal_token():
    """Get admin token for Kowalski to access SkyPortal"""
    result = subprocess.run(
        [
            "docker",
            "exec",
            "-i",
            "skyportal_web_1",
            "/bin/bash",
            "-c",
            "cat /skyportal/.tokens.yaml",
        ],
        cwd="skyportal",
        capture_output=True,
        universal_newlines=True,
    )
    token = result.stdout.split()[-1]

    if len(token) != 36:
        raise RuntimeError("Failed to get a SkyPortal token for Kowalski")

    return token


def build(
    init: bool = False,
    repo: str = "origin",
    branch: str = "master",
    traefik: bool = False,
    no_kowalski: bool = False,
    do_update: bool = False,
    skyportal_tag: str = "skyportal/web:latest",
    yes: bool = False,
):
    """Build Fritz

    :param init: Initialize Fritz
    :param repo: Remote repository to pull from
    :param branch: Branch on the remote repository
    :param traefik: Build Fritz to run behind Traefik
    :param no_kowalski: Do not build images for Kowalski
    :param do_update: pull <repo>/<branch>, autostash SP and update submodules
    :param skyportal_tag: Tag to apply to SkyPortal docker image
    :param yes: agree with all potentially asked questions
    """
    if do_update:
        update(init=init, repo=repo, branch=branch)

    if not env_ok:
        raise RuntimeError("Halting because of unsatisfied system dependencies")

    if not no_kowalski:
        # install Kowalski's deps:
        c = ["pip", "install", "-r", "requirements.txt"]
        subprocess.run(c, cwd="kowalski", check=True)

    # check config
    check_config(cfg="fritz.defaults.yaml", yes=yes)

    # load config
    with open("fritz.yaml") as fritz_config_yaml:
        fritz_config = yaml.load(fritz_config_yaml, Loader=yaml.FullLoader)

    patch_skyportal()

    # adjust F-specific docker-compose.yaml for SP
    with open("skyportal/docker-compose.skyportal.yaml") as docker_compose_yaml:
        docker_compose = yaml.load(docker_compose_yaml, Loader=yaml.FullLoader)
    # fix absolute paths in docker-compose.skyportal.yaml
    for vi, volume in enumerate(docker_compose["services"]["web"]["volumes"]):
        docker_compose["services"]["web"]["volumes"][vi] = volume.replace(
            "${PWD}", str(Path(__file__).parent.absolute())
        )
    if traefik:
        # fix host for Traefik
        docker_compose["services"]["web"]["labels"][2] = docker_compose["services"][
            "web"
        ]["labels"][2].replace("<host>", fritz_config["skyportal"]["server"]["host"])
    else:
        # not running behind Traefik? then publish port 5000 on host
        port = fritz_config["skyportal"]["server"].get("port", 5000)
        if port is None:
            port = 5000
        docker_compose["services"]["web"]["ports"] = [f"{port}:{port}"]
    # execute `make run` instead of `make run_production` at init:
    if init:
        docker_compose["services"]["web"][
            "command"
        ] = 'bash -c "source /skyportal_env/bin/activate && (make log &) && make run"'
    # save the adjusted version
    with open("skyportal/docker-compose.skyportal.yaml", "w") as docker_compose_yaml:
        yaml.dump(docker_compose, docker_compose_yaml)

    # Build skyportal's images
    cmd = ["docker", "build", "."]
    if skyportal_tag:
        cmd.extend(["-t", skyportal_tag])
    print(f"Building SkyPortal docker image (tag: {skyportal_tag})")
    p = subprocess.run(cmd, cwd="skyportal")
    if p.returncode != 0:
        raise RuntimeError("Failed to build skyportal's docker images")

    # when initializing, must start SP to generate token for K
    if init:
        # create common docker network (if it does not exist yet)
        p = subprocess.run(
            ["docker", "network", "create", "fritz_net"],
            capture_output=True,
            universal_newlines=True,
        )
        if (p.returncode != 0) and ("already exists" not in p.stderr):
            raise RuntimeError("Failed to create network fritz_net")

        # start up skyportal
        # docker-compose.skyportal.yaml bind-mounts the fritz-specific config.yaml and db_seed.yaml
        p = subprocess.run(
            ["docker-compose", "-f", "docker-compose.skyportal.yaml", "up", "-d"],
            cwd="skyportal",
            check=True,
        )
        if p.returncode != 0:
            raise RuntimeError("Failed to start SkyPortal")

        # init skyportal and load seed data
        mi, max_retires = 1, 3
        while mi <= max_retires:
            p = subprocess.run(
                [
                    "docker",
                    "exec",
                    "-i",
                    "skyportal_web_1",
                    "/bin/bash",
                    "-c",
                    "source /skyportal_env/bin/activate; make db_clear; make db_init;"
                    "make prepare_seed_data; make load_seed_data",
                ],
                cwd="skyportal",
            )
            if p.returncode == 0:
                break
            else:
                print("Failed to load seed data into SkyPortal, waiting to retry...")
                mi += 1
                time.sleep(15)
        if mi == max_retires + 1:
            raise RuntimeError("Failed to init SkyPortal and load seed data")

        # generate a token for Kowalski to talk to SkyPortal:
        with open("kowalski/config.yaml") as kowalski_config_yaml:
            config = yaml.load(kowalski_config_yaml, Loader=yaml.FullLoader)

        token = get_skyportal_token()
        config["kowalski"]["skyportal"]["token"] = token

        # save it to K's config:
        with open("kowalski/config.yaml", "w") as kowalski_config_yaml:
            yaml.dump(config, kowalski_config_yaml)

        # update fritz.yaml
        fritz_config["kowalski"]["skyportal"]["token"] = token
        with open("fritz.yaml", "w") as fritz_config_yaml:
            yaml.dump(fritz_config, fritz_config_yaml)

    if not no_kowalski:
        # Build kowalski's images
        c = ["python", "kowalski.py", "build"]
        if yes:
            c.append("--yes")
        p = subprocess.run(c, cwd="kowalski")
        if p.returncode != 0:
            raise RuntimeError("Failed to build Kowalski's docker images")

    if init:
        # stop SkyPortal
        subprocess.run(
            ["docker-compose", "-f", "docker-compose.skyportal.yaml", "down"],
            cwd="skyportal",
        )

        # remove common network
        subprocess.run(["docker", "network", "remove", "fritz_net"])


def develop():
    """Install tools for developing Fritz"""
    subprocess.run(["pre-commit", "install"])


def doc(yes: bool = False, upload: bool = False):
    """Build the documentation

    :param yes: agree to all potentially asked questions
    :param upload: Upload documentation to GitHub
    """
    check_config(yes=yes)
    for destination in ("config.yaml.defaults",):
        subprocess.run(
            [
                "cp",
                "config.yaml",
                destination,
            ],
            check=True,
            cwd="skyportal",
        )

    subprocess.run(["make", "html"], cwd="doc", check=True)

    patch_skyportal()

    env = os.environ.copy()
    env.update({"PYTHONPATH": "."})

    from baselayer.app.app_server import handlers as baselayer_handlers
    from skyportal.app_server import skyportal_handlers
    from skyportal.app_server_fritz import fritz_handlers
    from skyportal import openapi

    spec = openapi.spec_from_handlers(
        baselayer_handlers + skyportal_handlers + fritz_handlers,
        metadata={
            "title": "Fritz: SkyPortal API",
            "servers": [{"url": "https://fritz.science"}],
        },
    )
    with open("skyportal/openapi.json", "w") as f:
        json.dump(spec.to_dict(), f)

    subprocess.run(
        [
            "npx",
            "redoc-cli@0.9.8",
            "bundle",
            "openapi.json",
            "--title",
            "Fritz API docs",
            "--cdn",
            "--options.theme.logo.gutter",
            "2rem",
            "-o",
            "../doc/_build/html/api.html",
        ],
        check=True,
        cwd="skyportal",
    )
    os.remove("skyportal/openapi.json")

    if upload:
        subprocess.run(
            [
                "./tools/push_dir_to_repo.py",
                "--branch",
                "master",
                "--committer",
                "fritz",
                "--email",
                "fritz@fritz-marshal.org",
                "--message",
                "Update website",
                "--force",
                "./doc/_build/html",
                "git@github.com:fritz-marshal/doc",
            ],
            check=True,
        )


def lint():
    """Lint the full code base"""
    try:
        import pre_commit  # noqa: F401
    except ImportError:
        develop()

    try:
        subprocess.run(["pre-commit", "run", "--all-files"], check=True)
    except subprocess.CalledProcessError:
        sys.exit(1)


def log():
    """Show SkyPortal's colorized logs while Fritz is running"""
    p = subprocess.run(
        [
            "docker",
            "exec",
            "-i",
            "skyportal_web_1",
            "/bin/bash",
            "-c",
            "source /skyportal_env/bin/activate; make log",
        ],
        cwd="skyportal",
    )
    if p.returncode != 0:
        raise RuntimeError("Failed to display fritz's logs")


def prune(yes: bool = False):
    """☠️ Prune fritz's docker containers and volumes and reset configs to defaults

    :param yes: agree to all potentially asked questions
    """
    go = (
        input(
            "Do you want to prune Fritz's docker containers and volumes and deinit submodules? [y/N] "
        )
        if not yes
        else "y"
    )

    if go.lower() == "y":
        # try stopping anything that's running first:
        stop()

        # remove docker images
        for image_name in ("kowalski_api", "kowalski_ingester", "skyportal/web"):
            p1 = subprocess.Popen(["docker", "images"], stdout=subprocess.PIPE)
            p2 = subprocess.Popen(
                ["grep", image_name], stdin=p1.stdout, stdout=subprocess.PIPE
            )
            image_id = subprocess.check_output(
                ["awk", "{print $3}"], stdin=p2.stdout, universal_newlines=True
            ).strip()
            p3 = subprocess.run(["docker", "rmi", image_id])
            if p3.returncode == 0:
                print(f"Removed {image_name} docker image")
            else:
                print(f"Failed to remove {image_name} docker image")

        # remove docker volumes
        for volume_name in (
            "kowalski_data",
            "kowalski_mongodb",
            "skyportal_dbdata",
            "skyportal_thumbnails",
        ):
            p = subprocess.run(["docker", "volume", "rm", volume_name])
            if p.returncode == 0:
                print(f"Removed {volume_name} docker volume")
            else:
                print(f"Failed to remove {volume_name} docker volume")

        # deinit submodules
        p = subprocess.run(["git", "submodule", "deinit", "--all", "-f"])
        if p.returncode == 0:
            print("Deinitialized fritz's submodules")
        else:
            print("Failed to deinit fritz's submodules")


def run(
    init: bool = False,
    repo: str = "origin",
    branch: str = "master",
    traefik: bool = False,
    no_kowalski: bool = False,
    do_update: bool = False,
    skyportal_tag: str = "skyportal/web:latest",
    yes: bool = False,
):
    """🚀 Launch Fritz"""
    env = os.environ.copy()
    env.update({"FLAGS": "--config=../fritz.yaml"})

    if init:
        build(
            init=init,
            repo=repo,
            branch=branch,
            traefik=traefik,
            no_kowalski=no_kowalski,
            do_update=do_update,
            skyportal_tag=skyportal_tag,
            yes=yes,
        )

    # create common docker network (if it does not exist yet)
    p = subprocess.run(
        ["docker", "network", "create", "fritz_net"],
        capture_output=True,
        universal_newlines=True,
    )
    if (p.returncode != 0) and ("already exists" not in p.stderr):
        raise RuntimeError("Failed to create network fritz_net")

    # start up skyportal
    # docker-compose.skyportal.yaml bind-mounts the fritz-specific config.yaml and db_seed.yaml
    p = subprocess.run(
        ["docker-compose", "-f", "docker-compose.skyportal.yaml", "up", "-d"],
        cwd="skyportal",
        check=True,
    )
    if p.returncode != 0:
        raise RuntimeError("Failed to start SkyPortal")

    # start up kowalski
    c = ["python", "kowalski.py", "up"]
    p = subprocess.run(c, cwd="kowalski")
    if p.returncode != 0:
        raise RuntimeError("Failed to start Kowalski")

    if traefik:
        # check traefik's config
        check_config_exists(cfg="docker-compose.traefik.defaults.yaml", yes=yes)
        # fire up traefik
        p = subprocess.run(
            ["docker-compose", "-f", "docker-compose.traefik.yaml", "up", "-d"],
            check=True,
        )
        if p.returncode != 0:
            raise RuntimeError("Failed to start Traefik")


def stop():
    """✋ Shut Fritz down"""
    print("Shutting down Fritz...")

    # stop traefik if it is running
    running_container_images = (
        subprocess.check_output(["docker", "ps", "-a", "--format", "{{.Image}}"])
        .decode("utf-8")
        .strip()
        .split("\n")
    )
    traefik_is_running = any("traefik" in x.lower() for x in running_container_images)
    if traefik_is_running:
        print("Shutting down Traefik")
        subprocess.run(["docker-compose", "-f", "docker-compose.traefik.yaml", "down"])

    # stop Kowalski and SkyPortal
    subprocess.run(["python", "kowalski.py", "down"], cwd="kowalski")
    subprocess.run(
        ["docker-compose", "-f", "docker-compose.skyportal.yaml", "down"],
        cwd="skyportal",
    )

    # remove common network
    subprocess.run(["docker", "network", "remove", "fritz_net"])


def test():
    """Run the test suite"""
    print("Running integration testing...")

    # load config
    with open("fritz.yaml") as fritz_config_yaml:
        fritz_config = yaml.load(fritz_config_yaml, Loader=yaml.FullLoader)

    num_retries = 10
    # make sure the containers are up and running
    for i in range(num_retries):
        if i == num_retries - 1:
            raise RuntimeError("Fritz's containers failed to spin up")

        command = ["docker", "ps", "-a"]
        container_list = (
            subprocess.check_output(command, universal_newlines=True)
            .strip()
            .split("\n")
        )
        if len(container_list) == 1:
            print("No containers are running, waiting...")
            time.sleep(3)
            continue

        containers_up = (
            len(
                [
                    container
                    for container in container_list
                    if container_name in container and " Up " in container
                ]
            )
            > 0
            for container_name in (
                "kowalski_ingester_1",
                "kowalski_api_1",
                "skyportal_web_1",
            )
        )

        if not all(containers_up):
            print("Fritz's containers are not up, waiting...")
            time.sleep(3)
            continue

        break

    # make sure SkyPortal is running
    for i in range(num_retries):
        if i == num_retries - 1:
            raise RuntimeError("SkyPortal failed to spin up")

        command = ["docker", "exec", "-i", "skyportal_web_1", "ps", "-ef"]
        process_list = subprocess.check_output(command, universal_newlines=True).strip()

        if "app.py" not in process_list:
            print("SkyPortal is not up, waiting...")
            time.sleep(10)
            continue

        break

    # ensure that the SkyPortal app is responding to requests
    url = (
        f"{fritz_config['kowalski']['skyportal']['protocol']}://"
        f"localhost:{fritz_config['kowalski']['skyportal']['port']}"
        "/api/sysinfo"
    )
    token = fritz_config["kowalski"]["skyportal"]["token"]
    for i in range(num_retries):
        if i == num_retries - 1:
            raise RuntimeError("SkyPortal failed to spin up")
        try:
            response = skyportal_api("GET", endpoint=url, token=token)

            if response.status_code != 200:
                print("SkyPortal is not responding, waiting...")
                time.sleep(10)
            else:
                break

        except requests.exceptions.ConnectionError:
            print("SkyPortal is not responding, waiting...")
            time.sleep(10)
            continue

    print("Testing ZTF alert stream consumption and digestion")

    command = [
        "docker",
        "exec",
        "-i",
        "kowalski_ingester_1",
        "python",
        "-m",
        "pytest",
        "-s",
        "test_ingester.py",
    ]
    try:
        subprocess.run(command, check=True)
    except subprocess.CalledProcessError:
        sys.exit(1)

    # show processing log from dask cluster
    command = [
        "docker",
        "exec",
        "-i",
        "kowalski_ingester_1",
        "cat",
        "/data/logs/dask_cluster.log",
    ]
    try:
        subprocess.run(command, check=True)
    except subprocess.CalledProcessError:
        sys.exit(1)

    print("Testing Fritz-specific SkyPortal extensions")

    # use the generated config as test config for e.g. the correct db connection details
    command = [
        "docker",
        "exec",
        "-i",
        "skyportal_web_1",
        "cp",
        "config.yaml",
        "test_config.yaml",
    ]
    try:
        subprocess.run(command, check=True)
    except subprocess.CalledProcessError:
        sys.exit(1)

    api_tests = [
        test_file.name
        for test_file in Path("extensions/skyportal/skyportal/tests/api").glob(
            "test_*.py"
        )
    ]

    error = False
    for api_test in api_tests:
        command = [
            "docker",
            "exec",
            "-i",
            "skyportal_web_1",
            "/bin/bash",
            "-c",
            "source /skyportal_env/bin/activate &&"
            f"python -m pytest -s skyportal/tests/api/{api_test}",
        ]
        try:
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError:
            error = True
            continue
    if error:
        sys.exit(1)


def update(
    init: bool = False,
    repo: Optional[str] = None,
    branch: Optional[str] = None,
):
    """Update Fritz

    :param init: Initialize before updating Fritz
    :param repo: Remote repository to pull from
    :param branch: Branch on the remote repository
    """
    git_pull_command = ["git", "pull"]
    if repo is not None and branch is not None:
        git_pull_command.extend([repo, branch])
    p = subprocess.run(git_pull_command)
    if p.returncode != 0:
        raise RuntimeError("Failed to git pull Fritz")

    if init:
        # initialize/update fritz's submodules kowalski and skyportal
        # pull skyportal and kowalski
        p = subprocess.run(["git", "submodule", "update", "--init", "--recursive"])
        if p.returncode != 0:
            raise RuntimeError("Failed to initialize fritz's submodules")
    # auto stash SP
    p = subprocess.run(["git", "stash"], cwd="skyportal")
    if p.returncode != 0:
        raise RuntimeError("SkyPortal autostash failed")

    # update submodules
    p = subprocess.run(["git", "submodule", "update", "--recursive"])
    if p.returncode != 0:
        raise RuntimeError("Failed to update fritz's submodules")


if __name__ == "__main__":
    # check environment
    initialize_submodules()
    env_ok = dependencies_ok()
    if not env_ok:
        raise RuntimeError("Halting because of unsatisfied dependencies")

    import fire

    fire.Fire(
        {
            "run": run,
            "stop": stop,
            "build": build,
            "update": update,
            "test": test,
            "develop": develop,
            "lint": lint,
            "prune": prune,
            "doc": doc,
        }
    )
