#!/usr/bin/env python3
"""Use the app's committed Ruby bundle for native preparation without changing the job environment."""
from __future__ import annotations

import argparse
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile

import native_dependency_guard as guard
from native_pods_diagnostics import refuse
import native_pods_process
from native_prepare_evidence import report


# The Ruby bundle is only one of the things this script can refuse over, but every
# refusal used to carry the lockfile remediation below. printer-ai run 34374868740
# failed because CocoaPods could not clone a dependency from github.com, and the
# receipt told the session to "reconcile dependency locks locally" — locks that were
# never wrong. A configuration command that fails inside its own child publishes its
# own receipt and signals; point the reader at those instead (2026-09-09).
BUNDLE_REMEDIATION = ("Use the app's committed Gemfile and Gemfile.lock with a compatible Ruby/Bundler "
                      "runtime; reconcile dependency locks locally and retain deployment guards.")
COMMAND_REMEDIATION = ("The configuration command failed inside its own child, not in the Ruby bundle. "
                       "Read that child's own receipt and native_prepare_failed signals for the cause, "
                       "and repair what they name.")
BUNDLE_CODES = frozenset({
    "native_gemfile_required", "native_gemfile_commit_required", "native_gemfile_lock_required",
    "native_gemfile_lock_commit_required", "native_bundle_install_failed", "native_bundle_inputs_changed",
})


def remediation(code: str) -> str:
    return BUNDLE_REMEDIATION if code in BUNDLE_CODES else COMMAND_REMEDIATION


def command(argv: list[str], cwd: Path, log, *, timeout: float = 900) -> int:
    try:
        return native_pods_process.command(argv, cwd, log, timeout=timeout)
    except native_pods_process.Interrupted:
        refuse("native_pods_operation_interrupted", log, guard.Refused)
    except (OSError, RuntimeError, subprocess.SubprocessError):
        raise guard.Refused("native_pods_process_unverified") from None


def committed(root: Path, path: Path, kind: str) -> str:
    if not path.is_file() or path.is_symlink() or path.stat().st_size > 2 * 1024 * 1024:
        raise guard.Refused(f"native_{kind}_required")
    if not path.resolve().is_relative_to(root):
        raise guard.Refused("native_bundle_path_unverified")
    result = subprocess.run(["git", "show", "HEAD:" + path.relative_to(root).as_posix()],
                            cwd=root, capture_output=True, check=False, timeout=15)
    if result.returncode or result.stdout != path.read_bytes():
        raise guard.Refused(f"native_{kind}_commit_required")
    return hashlib.sha256(result.stdout).hexdigest()


def unchanged(snapshot: dict[Path, str]) -> None:
    for path, digest in snapshot.items():
        if not path.is_file() or path.is_symlink() or hashlib.sha256(path.read_bytes()).hexdigest() != digest:
            raise guard.Refused("native_bundle_inputs_changed")


def prepare(root: Path, folder: Path, log) -> tuple[list[str], dict[Path, str]]:
    gemfile = next((p for p in (folder / "Gemfile", root / "Gemfile") if p.exists() or p.is_symlink()), None)
    if gemfile is None:
        return [], {}
    snapshot = {gemfile: committed(root, gemfile, "gemfile"),
                gemfile.with_name("Gemfile.lock"): committed(root, gemfile.with_name("Gemfile.lock"), "gemfile_lock")}
    prefix = ["/usr/bin/env", "BUNDLE_GEMFILE=" + str(gemfile), "BUNDLE_FROZEN=true",
              "BUNDLE_PATH=" + (os.environ.get("BUNDLE_PATH") or str(root / ".factory/toolchain/gems"))]
    relay = [*prefix, "bundle"]
    if command([*relay, "check"], gemfile.parent, log):
        if command([*relay, "install", "--jobs", "2", "--retry", "1"], gemfile.parent, log):
            refuse("native_bundle_install_failed", log, guard.Refused)
    unchanged(snapshot)
    return [*prefix, "bundle", "exec"], snapshot


def run(root: Path, folder: Path, argv: list[str]) -> dict:
    root = root.resolve(strict=True)
    folder = (root / folder).resolve(strict=True)
    if not folder.is_relative_to(root) or not argv:
        raise guard.Refused("native_bundle_path_unverified")
    if (folder / "Podfile").is_symlink():
        raise guard.Refused("native_workspace_reference_unverified")
    pod_lock = folder / "Podfile.lock"
    pod_snapshot = {pod_lock: committed(root, pod_lock, "podfile_lock")} if (folder / "Podfile").exists() else {}
    with tempfile.TemporaryFile() as log:
        prefix, snapshot = prepare(root, folder, log)
        if command([*prefix, *argv], root, log):
            refuse("native_configuration_failed", log, guard.Refused)
        unchanged(snapshot)
        for path, digest in pod_snapshot.items():
            if not path.is_file() or path.is_symlink() or hashlib.sha256(path.read_bytes()).hexdigest() != digest:
                raise guard.Refused("native_podfile_lock_changed")
    return {"ok": True, "ruby_bundle": "locked" if snapshot else "not_configured"}


def interrupted(*_args) -> None:
    raise KeyboardInterrupt


def main(argv: list[str]) -> int:
    signal.signal(signal.SIGTERM, interrupted)
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root", type=Path, default=Path.cwd())
    parser.add_argument("--folder", type=Path, default=Path("ios"))
    parser.add_argument("command", nargs=argparse.REMAINDER)
    args = parser.parse_args(argv)
    command_args = args.command[1:] if args.command[:1] == ["--"] else args.command
    try:
        answer = run(args.root, args.folder, command_args)
    except (guard.Refused, OSError, subprocess.SubprocessError) as exc:
        code = str(exc) if isinstance(exc, guard.Refused) else "native_bundle_operation_unverified"
        answer = {"ok": False, "code": code, "next": remediation(code)}
        if getattr(exc, "diagnostic", ""):
            answer["diagnostic"] = exc.diagnostic
    report(answer, "bundle")
    print(json.dumps(answer, sort_keys=True))
    return 0 if answer["ok"] else 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
