#!/usr/bin/env python3

# Since this can't run under Bazel, we have to set up the sys.path ourselves
import os
import sys
from pathlib import Path

if (p := str(Path(sys.argv[0]).absolute().parent.parent)) not in sys.path:
    sys.path.pop(0)  # Remove '.' / ''
    sys.path.insert(0, p)  # Insert the bazelshim root

# Now that's out of the way...
from dataclasses import dataclass
from shlex import quote, split as shlex
import sys
import tomllib
from pathlib import Path
from typing import List, Optional
from itertools import chain


# FIXME: Allow user-defined extensions here
VERBS = [
    "aquery",
    "build",
    "clean",
    "coverage",
    "cquery",
    "dump",
    "fetch",
    "help",
    "info",
    "mod",
    "query",
    "run",
    "sync",
    "test",
    "watch",
]


def path():
    for it in os.getenv("PATH").split(":"):
        yield Path(it)


def which(cmd):
    for it in path():
        f: Path = (it / cmd).absolute()
        if f.exists() and f.stat().st_mode & 0x700:
            yield f


def normalize_opts(args: List[str]) -> List[str]:
    acc = []

    if args[0].endswith("bazel") or args[0].endswith("bazelis"):
        acc.append(args.pop(0))

    while len(args) >= 2:
        if args[0] == "--":
            # Break
            acc.extend(args)
            break

        elif "=" in args[0]:
            # If it's a k/v form pass it through
            acc.append(args.pop(0))

        elif args[0].startswith("--no"):
            # Convert --no<foo> args to --<foo>=no
            acc.append("--" + args.pop(0).lstrip("--no") + "=false")

        elif args[0] == "--isatty=0":
            acc.append("--isatty=false")
            args.pop(0)

        elif (
            args[0].startswith("--")
            and not args[1].startswith("--")
            and args[1] not in VERBS
        ):
            # If the next thing isn't an opt, assume it's a '--a b' form
            acc.append(args[0] + "=" + args[1])
            args.pop(0)
            args.pop(0)

        elif args[0].startswith("--"):
            # Assume it's a boolean true flag
            acc.append(args.pop(0) + "=true")

        else:
            acc.append(args.pop(0))

    else:
        if args:
            acc.extend(args)

    return acc


@dataclass
class BazelCli:
    binary: str
    startup_opts: List[str]
    command: Optional[str]
    command_opts: List[str]
    subprocess_opts: List[str]

    @classmethod
    def parse_cli(cls, args: List[str]) -> "BazelCli":
        args = normalize_opts(args)
        binary = args.pop(0)

        startup_opts = []
        while args and args[0].startswith("--"):
            startup_opts.append(args.pop(0))

        command = None
        if args and args[0] in VERBS:
            command = args.pop(0)

        command_opts = []
        while args and args[0] != "--":
            command_opts.append(args.pop(0))

        subprocess_opts = []
        if args:
            if args[0] == "--":
                args.pop(0)
            subprocess_opts.extend(args)

        return cls(
            binary=binary,
            startup_opts=startup_opts,
            command=command,
            command_opts=command_opts,
            subprocess_opts=subprocess_opts,
        )

    def render_cli(self):
        acc = [
            self.binary,
            *self.startup_opts,
        ]
        if self.command:
            if self.command == "watch":
                acc.extend(self.command_opts)
            else:
                acc.append(self.command)
                acc.extend(self.command_opts)

            if self.command == "test":
                acc.extend(["--test_arg=" + it for it in self.subprocess_opts])
            elif self.command == "run":
                acc.append("--")
                acc.extend(self.subprocess_opts)
            elif self.command and not self.subprocess_opts:
                pass
            else:
                print(
                    f"Warning: {self.command} does not support -- args! {self.subprocess_opts!r}",
                    file=sys.stderr,
                )

        return acc

    def executable(self, exclude: List[Path]):
        """Try to resolve as via which() an executable to delegate to."""

        if self.command == "watch":
            for p in chain(which("ibazel")):
                if p.parent not in exclude:
                    return str(p)

        else:
            for p in chain(which("bazelisk"), which("bazel")):
                if p.parent not in exclude:
                    return str(p)

    def render_text(self, next):
        lines = []
        lines.append("  " + next)
        base_prefix = "      "
        for arg in self.render_cli()[1:]:
            prefix = base_prefix
            if arg in VERBS or arg == self.command:
                prefix = "    "
            elif arg == "--":
                base_prefix += "  "
            lines.append(prefix + arg)
        return "\\\n".join(lines)


# FIXME: Use some sort of plugin model here to implement interceptors
def middleware(cli):
    return cli


if __name__ == "__main__":
    # This script has a magical flag to help with resolving bazel
    exclude = []
    while len(sys.argv) > 1 and sys.argv[1].startswith("--bazelshim_exclude"):
        exclude.append(Path(sys.argv.pop(1).split("=")[1]).absolute())

    us = Path(sys.argv[0]).absolute()
    exclude.append(us.parent)

    cli = BazelCli.parse_cli(["bazel"] + sys.argv[1:])
    cli = middleware(cli)
    next = cli.executable(exclude=exclude)
    if sys.stderr.isatty() and not "--isatty=false" in cli.command_opts:
        print(
            "\u001b[33mInfo\u001b[0m: Executing\n" + cli.render_text(next),
            file=sys.stderr,
        )
    os.execv(next, cli.render_cli())
