#!/usr/bin/env python3
"""The eval run's `shipmill` and `uvx` (spec 025, #494): `shipmill gh` goes to the gh stub.

A skill in a repository whose config sets `[agents] app_id` writes through `shipmill gh`,
as `uvx shipmill==<the plugin's version> gh ...` (spec 031), or another uvx form. The
real `shipmill gh` needs the App's token and refuses without one, so in an offline run no
write would reach the stub. This shim, on PATH as `shipmill` and (linked) as `uvx`, sends

    shipmill [--repo PATH] gh <args>
    uvx [options] shipmill[==V | @V] [--repo PATH] gh <args>

to the sibling `gh` stub with <args>, which logs the call with `"via": "shipmill"`; a plain
`gh` call lacks it, so a grader still tells a write through `shipmill gh` from a write as a
person. `shipmill [--repo PATH] app-token --check` (spec 033), in either form, answers as
the real check would where the App is available: exit 0, and a `{"kind": "check"}` line in
`.eval/gh.jsonl`. A case whose fixture holds a file `no-app` runs without the App: the check
exits 2 with the real check's reason, and every `shipmill gh` call exits 2 with the real
missing-key message, logged as `{"kind": "refused"}`, and never reaches the stub. Every
other command (`uvx --from actionlint-py actionlint`, `shipmill product show`) runs the real
program unchanged: the first one on PATH that isn't this shim.

Python 3.10 compatible, standard library only, like the skills' scripts.
"""

import json
import os
import shutil
import sys
from pathlib import Path

NOT_AVAILABLE = (
    "the App isn't available here, so shipmill writes nothing: a skill hands you its text to post, "
    "and what you post yourself with gh is yours (spec 033)"
)

# uvx options that take a value as the next argument; any other option is a flag
UVX_VALUES = {
    "--from",
    "-w",
    "--with",
    "--with-editable",
    "--with-requirements",
    "-c",
    "--constraints",
    "-b",
    "--build-constraints",
    "--overrides",
    "--env-file",
    "--python-platform",
    "--torch-backend",
    "--index",
    "--default-index",
    "-i",
    "--index-url",
    "--extra-index-url",
    "-f",
    "--find-links",
    "--index-strategy",
    "--keyring-provider",
    "-P",
    "--upgrade-package",
    "--upgrade-group",
    "--resolution",
    "--prerelease",
    "--prerelease-package",
    "--fork-strategy",
    "--exclude-newer",
    "--exclude-newer-package",
    "--no-sources-package",
    "--reinstall-package",
    "--link-mode",
    "-C",
    "--config-setting",
    "--config-settings-package",
    "--no-build-isolation-package",
    "--no-build-package",
    "--no-binary-package",
    "--cache-dir",
    "--refresh-package",
    "-p",
    "--python",
    "--color",
    "--allow-insecure-host",
    "--directory",
    "--project",
    "--config-file",
}


def gh_args(argv: list[str]) -> list[str] | None:
    """The arguments after `gh` in a shipmill command line, as shipmill's CLI splits it:
    only `--repo PATH` (or an abbreviation, or `--repo=PATH`) may come before gh"""
    index = 0
    while index < len(argv):
        arg = argv[index]
        if arg == "gh":
            return argv[index + 1 :]
        if not arg.startswith("-") or arg == "--":
            return None
        index += 2 if len(arg) > 2 and "--repo".startswith(arg) else 1
    return None


def uvx_command(argv: list[str]) -> int | None:
    """The index of the command uvx would run, past its options; None when there is none"""
    index = 0
    while index < len(argv):
        arg = argv[index]
        if not arg.startswith("-"):
            return index
        index += 2 if arg in UVX_VALUES else 1
    return None


def is_shipmill(command: str) -> bool:
    return command == "shipmill" or command.startswith(("shipmill==", "shipmill@"))


def shipmill_args(name: str, argv: list[str]) -> list[str] | None:
    """The arguments shipmill gets, run as `shipmill` or through uvx; None for another program"""
    if name == "uvx":
        at = uvx_command(argv)
        if at is None or not is_shipmill(argv[at]):
            return None
        return argv[at + 1 :]
    return argv


def stubbed(name: str, argv: list[str]) -> list[str] | None:
    args = shipmill_args(name, argv)
    return None if args is None else gh_args(args)


def is_check(args: list[str]) -> bool:
    """`[--repo PATH] app-token --check`, as shipmill's CLI reads it"""
    index = 0
    while index < len(args) and args[index].startswith("-") and args[index] != "--":
        index += 2 if len(args[index]) > 2 and "--repo".startswith(args[index]) else 1
    return args[index:] == ["app-token", "--check"]


def find_eval(start: Path) -> Path | None:
    for directory in (start, *start.parents):
        if (directory / ".eval").is_dir():
            return directory / ".eval"
    return None


def log(eval_dir: Path, line: dict[str, object]) -> None:
    with (eval_dir / "gh.jsonl").open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(line, ensure_ascii=False) + "\n")


def missing_key() -> str:
    return str(Path(os.environ.get("HOME", "~")) / ".config" / "shipmill" / "app-<app_id>.pem")


def answer_without_app(eval_dir: Path, args: list[str], gh: list[str] | None) -> int:
    """The check, or a `shipmill gh` call, in a case run without the App (fixture/no-app)"""
    if gh is None:
        log(eval_dir, {"kind": "check", "args": args})
        sys.stderr.write(f"shipmill: the App isn't available here: no key at {missing_key()} and no SHIPMILL_APP_KEY\n")
        return 2
    log(eval_dir, {"kind": "refused", "via": "shipmill", "args": gh})
    sys.stderr.write(
        f"shipmill: no App private key at {missing_key()}; download it from the App's settings to that path\n"
        f"{NOT_AVAILABLE}\n"
    )
    return 2


def real(name: str) -> str:
    """The first `name` on PATH after this shim's own entry that isn't this shim, linked or not"""
    me = Path(__file__).resolve()
    entries = [e for e in os.environ.get("PATH", "").split(os.pathsep) if e]
    found = [shutil.which(name, path=e) for e in entries]
    mine = [i for i, f in enumerate(found) if f is not None and Path(f).resolve() == me]
    for candidate in found[mine[0] + 1 if mine else 0 :]:
        if candidate is not None and Path(candidate).resolve() != me:
            return candidate
    sys.stderr.write(f"{name}: no real {name} on PATH after {me.parent}\n")
    sys.exit(127)


def main(argv: list[str]) -> None:
    name = Path(argv[0]).name
    if name not in ("shipmill", "uvx"):
        sys.exit(f"shim: run as shipmill or uvx, not {name}")
    given = shipmill_args(name, argv[1:])
    checking = given is not None and is_check(given)
    args = stubbed(name, argv[1:])
    eval_dir = find_eval(Path.cwd()) if checking or args is not None else None
    if eval_dir is not None and (eval_dir / "no-app").is_file():
        sys.exit(answer_without_app(eval_dir, given or [], None if checking else args))
    if checking and eval_dir is not None:
        log(eval_dir, {"kind": "check", "args": given})
        sys.stdout.write("the App is available here: SHIPMILL_APP_KEY is set (an eval run)\n")
        sys.exit(0)
    if args is None:
        program = real(name)
        os.execv(program, [program, *argv[1:]])
    stub = Path(__file__).resolve().parent / "gh"
    os.environ["EVAL_GH_VIA"] = "shipmill"
    os.execv(sys.executable, [sys.executable, str(stub), *args])


if __name__ == "__main__":
    main(sys.argv)
