#!/usr/bin/env python3

# stdlib imports
import argparse
import os
import shutil
import subprocess
import sys
import textwrap

# stdlib "from" imports
from argparse import ArgumentParser, Namespace
from pathlib import Path

# "local" imports
from config import Config
from typing import Optional

_SCRIPT_NAME = Path(sys.argv[0]).name
_SCRIPT_DIR = Path(sys.argv[0]).parent.resolve()
_TOP_LEVEL = Path(
    subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
)


class ChutneyBinResolver:
    _DEST = "chutney_bin"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument("--chutney-bin", dest=self._DEST, help="chutney executable")

    def resolve(self, args: Namespace) -> Path:
        res_str = getattr(args, self._DEST) or shutil.which("chutney")
        if res_str is None:
            print(textwrap.dedent("""
                    chutney (https://gitlab.torproject.org/tpo/core/chutney) not found.
                    Try setting --chutney-bin or ensuring it's on your PATH.
                    quick install:
                    python3 -m pip install git+https://gitlab.torproject.org/tpo/core/chutney.git
                    """))
            sys.exit(1)
        res = Path(res_str)
        if not os.access(res, os.X_OK):
            print(f"chutney resolved to {res}, but it isn't an executable file")
            sys.exit(1)
        return res


class ArtiBinResolver:
    _DEST = "arti_bin"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument("--arti-bin", dest=self._DEST, help="arti executable")

    def resolve(self, args: Namespace) -> Path:
        res_str = getattr(args, self._DEST)
        if res_str is None:
            for s in [
                "target/x86_64-unknown-linux-gnu/quicktest/arti",
                "target/quicktest/arti",
                "target/x86_64-unknown-linux-gnu/debug/arti",
                "target/debug/arti",
            ]:
                p = _TOP_LEVEL.joinpath(Path(s))
                if p.exists():
                    res_str = str(p)
                    break
        if res_str is None:
            print(textwrap.dedent("""
                    arti client not found.
                    Try setting --arti-bin or building one in this repo:
                    cargo build --locked --profile=quicktest -p arti
                    """))
            sys.exit(1)
        res = Path(res_str)
        if not os.access(res, os.X_OK):
            print(f"arti resolved to {res}, but it isn't an executable file")
            sys.exit(1)
        return res


class ArtiExtraBinResolver:
    _DEST = "arti_extra_bin"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--arti-extra-bin",
            dest=self._DEST,
            help="arti executable with extra features",
        )

    def resolve(self, args: Namespace) -> Path:
        res_str = getattr(args, self._DEST)
        if res_str is None:
            for s in [
                "target/x86_64-unknown-linux-gnu/quicktest/arti-extra",
                "target/quicktest/arti-extra",
                "target/x86_64-unknown-linux-gnu/debug/arti-extra",
                "target/debug/arti-extra",
            ]:
                p = _TOP_LEVEL.joinpath(Path(s))
                if p.exists():
                    res_str = str(p)
                    break
        if res_str is None:
            print(textwrap.dedent("""
                    arti-extra client not found.
                    Try setting --arti-extra-bin or building one in this repo.

                    # See .rust-recent-arti-extra-features-template in .gitlab-ci.yml for current
                    # --features list.
                    $ cargo build --locked --profile=quicktest -p arti --features=full,experimental
                    $ mv target/quicktest/arti target/quicktest/arti-extra
                    """))
            sys.exit(1)
        res = Path(res_str)
        if not os.access(res, os.X_OK):
            print(f"arti resolved to {res}, but it isn't an executable file")
            sys.exit(1)
        return res


class ArtiBenchBinResolver:
    _DEST = "arti_bench_bin"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--arti-bench-bin", dest=self._DEST, help="arti-bench executable"
        )

    def resolve(self, args: Namespace) -> Path:
        res_str = getattr(args, self._DEST)
        if res_str is None:
            for s in [
                "target/x86_64-unknown-linux-gnu/release/arti-bench",
                "target/release/arti-bench",
                "target/x86_64-unknown-linux-gnu/quicktest/arti-bench",
                "target/quicktest/arti-bench",
                "target/x86_64-unknown-linux-gnu/debug/arti-bench",
                "target/debug/arti-bench",
            ]:
                p = _TOP_LEVEL.joinpath(Path(s))
                if p.exists():
                    res_str = str(p)
                    break
        if res_str is None:
            print(textwrap.dedent("""
                    arti-bench not found.
                    Try setting --arti-bench-bin or building one in this repo:
                    cargo build --locked --profile=quicktest -p arti-bench
                    """))
            sys.exit(1)
        res = Path(res_str)
        if not os.access(res, os.X_OK):
            print(f"arti-bench resolved to {res}, but it isn't an executable file")
            sys.exit(1)
        return res


class ChutneyDataDirResolver:
    _DEST = "chutney_data_dir"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--chutney-data-dir",
            dest=self._DEST,
            help="directory for chutney to put its data",
        )

    def resolve(self, args: Namespace) -> Path:
        return Path(
            getattr(args, self._DEST) or os.getenv("CHUTNEY_DATA_DIR") or os.getcwd()
        )


class ChutneyNetworkResolver:
    _DEST = "network"

    def __init__(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--network",
            "-n",
            dest=self._DEST,
            help="flag to 'chutney init' specifying network to use."
            " (Default: built-in network-builder)",
        )

    def resolve(self, args: Namespace) -> Optional[str]:
        res: Optional[str] = getattr(args, self._DEST)
        return res


class ConfigResolver:
    def __init__(self, parser: ArgumentParser) -> None:
        self._chutney_resolver = ChutneyBinResolver(parser)
        self._arti_resolver = ArtiBinResolver(parser)
        self._arti_extra_resolver = ArtiExtraBinResolver(parser)
        self._arti_bench_resolver = ArtiBenchBinResolver(parser)
        self._chutney_data_dir_resolver = ChutneyDataDirResolver(parser)
        self._network_resolver = ChutneyNetworkResolver(parser)

    def resolve(self, args: Namespace) -> Config:
        return Config(
            chutney=str(self._chutney_resolver.resolve(args)),
            arti=str(self._arti_resolver.resolve(args)),
            arti_extra=str(self._arti_extra_resolver.resolve(args)),
            arti_bench=str(self._arti_bench_resolver.resolve(args)),
            chutney_data_dir=str(self._chutney_data_dir_resolver.resolve(args)),
            network=self._network_resolver.resolve(args),
        )


def _main() -> None:
    # Set up command-line parser, registering config options
    parser = argparse.ArgumentParser(
        prog=_SCRIPT_NAME, description="Configure a chutney testbed"
    )
    config_resolver = ConfigResolver(parser)
    args = parser.parse_args()
    config = config_resolver.resolve(args)
    config.dump_json(_SCRIPT_DIR.joinpath("arti.run.json"))


if __name__ == "__main__":
    _main()
