#!/usr/bin/env python3
import json
import pickle
import subprocess

from argparse import ArgumentParser
from os import chdir, getenv
from os.path import exists, join
from subprocess import PIPE

import semver
import tomlkit
from colorama import Fore, Style
from prettytable import PrettyTable

# Path to git repository holding crates.io index
CRATESIO_REPO = "https://github.com/rust-lang/crates.io-index"
CRATESIO_INDEX = join(getenv("HOME"), ".cache", "crates.io-index")

# Path to pickle cache for storing paths to dependency metadata
PICKLE_CACHE = join(getenv("HOME"), ".cache", "cargo-outdated.pickle")

# Set of packages that are ignored by this tool
IGNORES = {
    "darkfi-serial",
    "darkfi-sdk",
    "darkfi",
    "darkfi-derive",
    "darkfi-derive-internal",
    "dao-contract",
    "money-contract",
}

# Yanked releases from crates.io to ignore
YANKED = {}

# Cached paths for metadata to not have to search through the crates index
METADATA_PATHS = {}

if exists(PICKLE_CACHE):
    with open(PICKLE_CACHE, "rb") as f:
        json_data = pickle.load(f)
        METADATA_PATHS = json.loads(json_data)


def parse_toml(filename):
    with open(filename) as f:
        content = f.read()

    p = tomlkit.parse(content)
    deps = p.get("dependencies")
    devdeps = p.get("dev-dependencies")

    if deps and devdeps:
        dependencies = deps | devdeps
    elif deps:
        dependencies = deps
    elif devdeps:
        dependencies = devdeps
    else:
        dependencies = None

    return (p, dependencies)


def get_metadata_path(name):
    find_output = subprocess.run(
        ["find", CRATESIO_INDEX, "-type", "f", "-name", name], stdout=PIPE)

    metadata_path = find_output.stdout.decode().strip()

    if metadata_path == '':
        return None

    # Place the path into cache
    METADATA_PATHS[name] = metadata_path
    return metadata_path


def check_dep(name, data):
    if name in IGNORES:
        return None

    metadata_path = METADATA_PATHS.get(name)

    if not metadata_path:
        metadata_path = get_metadata_path(name)
        if not metadata_path:
            print(f"No crate found for {Fore.YELLOW}{name}{Style.RESET_ALL}")
            return None

    # Read the metadata. It's split as JSON objects, each in its own line.
    with open(metadata_path, encoding="utf-8") as f:
        lines = f.readlines()
        lines = [i.strip() for i in lines]

    # Latest one is at the end
    metadata = json.loads(lines[-1])

    # Get the version from the local data
    if isinstance(data, str):
        # This is just the semver
        local_version = data
    elif isinstance(data, dict):
        local_version = data.get("version")
        if not local_version:
            # Not a versioned dependency (can be path/git/...)
            return None
    else:
        raise ValueError(f"Invalid dependency: {name}")

    try:
        if semver.compare(local_version, metadata["vers"]) < 0:
            name = metadata["name"]
            vers = metadata["vers"]

            if name in YANKED and vers in YANKED[name]:
                return None

            return (local_version, vers)
    except:
        return None

    return None


def main():
    parser = ArgumentParser(
        description="Prettyprint outdated dependencies in a cargo project")

    parser.add_argument("-u",
                        "--update",
                        action="store_true",
                        help="Prompt to update dependencies")
    parser.add_argument("-i",
                        "--ignore",
                        type=str,
                        help="Comma-separated list of deps to ignore")

    args = parser.parse_args()

    if args.ignore:
        for i in args.ignore.split(","):
            IGNORES.add(i)

    if not exists(CRATESIO_INDEX):
        print("Cloning crates.io index...")
        subprocess.run(["git", "clone", CRATESIO_REPO, CRATESIO_INDEX],
                       capture_output=False)

    print("Updating crates.io index...")
    subprocess.run(["git", "-C", CRATESIO_INDEX, "fetch", "-a"],
                   capture_output=False)

    subprocess.run(
        ["git", "-C", CRATESIO_INDEX, "reset", "--hard", "origin/master"],
        capture_output=False)

    # chdir to the root of the project
    toplevel = subprocess.run(["git", "rev-parse", "--show-toplevel"],
                              capture_output=True)
    toplevel = toplevel.stdout.decode().strip()
    chdir(toplevel)

    find_output = subprocess.run(
        ["find", ".", "-type", "f", "-name", "Cargo.toml"], stdout=PIPE)
    files = [i.strip() for i in find_output.stdout.decode().split("\n")][:-1]

    x = PrettyTable()
    x.field_names = ["package", "crate", "current", "latest", "path"]

    for filename in files:
        ps, deps = parse_toml(filename)
        package = ps["package"]["name"]
        print(f"Checking deps for {Fore.GREEN}{package}{Style.RESET_ALL}")
        for dep in deps:
            ret = check_dep(dep, deps[dep])
            if ret:
                x.add_row([
                    package,
                    dep,
                    f"{Fore.YELLOW}{ret[0]}{Style.RESET_ALL}",
                    f"{Fore.GREEN}{ret[1]}{Style.RESET_ALL}",
                    filename,
                ])

            if args.update and ret:
                print(f"Update {dep} from {ret[0]} to {ret[1]}? (y/N): ",
                      end="")
                choice = input()
                if choice and (choice == "y" or choice == "Y"):
                    if "dependencies" in ps and dep in ps["dependencies"]:
                        if ps["dependencies"][dep] == ret[0]:
                            ps["dependencies"][dep] = ret[1]
                        else:
                            ps["dependencies"][dep]["version"] = ret[1]
                    elif "dev-dependencies" in ps and dep in ps[
                            "dev-dependencies"]:
                        if ps["dev-dependencies"][dep] == ret[0]:
                            ps["dev-dependencies"][dep] = ret[1]
                        else:
                            ps["dev-dependencies"][dep]["version"] = ret[1]

        if args.update:
            with open(filename, "w") as f:
                f.write(tomlkit.dumps(ps))

    print(x)

    # Write the pickle
    with open(PICKLE_CACHE, "wb") as pfile:
        pickle.dump(json.dumps(METADATA_PATHS).encode(), pfile)


if __name__ == "__main__":
    main()
