# prevent/install.py
#!/usr/bin/env python3
"""install.py — idempotently CHAIN the deterministic prevent gate into a repo's EXISTING pre-commit hook,
NEVER clobbering it. audience: AI coding agents first.

Design (BLUF):
- One uniform POSIX-sh marker block (`# prevent-band-hook v1 BEGIN … END`), appended AFTER the existing hook
  logic. Mechanism-detect picks only the target FILE — it does NOT rewrite the existing hook.
- NO-SWALLOW: the block's first statement captures the prior command's exit (`_pb_rc=$?`) and re-exits on
  failure, so an existing blocking hook (slopgate / lint-staged) still aborts. A naive append would mask it.
- FAIL-OPEN: the prevent call is guarded by `[ -f <PREVENT_PY> ]`, so a clone / CI without security-gate
  installed at this path NO-OPS instead of hard-failing every commit (the block hardcodes an absolute path).
- IDEMPOTENT: re-running REPLACES the block in place (not skip-if-present), so flipping report-only↔enforce
  by re-install actually updates the invocation.
- REVERSIBLE: `--uninstall` strips the block, leaving the original hook intact.

Mechanism → target file:
- husky (`core.hooksPath` ends in `_`): the authored, COMMITTED hook is `<repo>/.husky/pre-commit` (husky v9
  owns only `.husky/_/`, never the authored file) — editing it is a TRACKED change the user must commit.
- explicit `core.hooksPath`: `<hooksPath>/pre-commit`.
- default: `<repo>/.git/hooks/pre-commit` (local, untracked).
"""
import argparse, os, stat, subprocess, sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # security-gate install root
PREVENT_PY = os.path.join(ROOT, "prevent", "prevent.py")
BEGIN = "# prevent-band-hook v1 BEGIN"
END = "# prevent-band-hook v1 END"


def block(report_only):
    """The uniform POSIX-sh chain block. report_only adds --report-only (monitor; never aborts)."""
    inv = f'python3 "{PREVENT_PY}" --trigger pre-commit' + (" --report-only" if report_only else "")
    return (
        f"{BEGIN} — security-gate deterministic prevent ({'report-only' if report_only else 'ENFORCE'}). "
        f"Strip this block (or run prevent/install.py --uninstall) to remove.\n"
        f'_pb_rc=$?; [ "$_pb_rc" -ne 0 ] && exit "$_pb_rc"  # preserve prior hook command exit (no-swallow)\n'
        f'if [ -f "{PREVENT_PY}" ]; then  # fail-open: absent on this clone/CI -> no-op, never wedge the commit\n'
        f"  {inv}\n"
        f"fi\n"
        f"{END}\n"
    )


def hook_target(repo_root):
    """Return (path, mechanism) of the shell hook git ACTUALLY runs — reading the file the inert one shadows
    is the dead-hook trap. husky redirects core.hooksPath into .husky/_, but the authored hook is one level up."""
    hp = subprocess.run(["git", "-C", repo_root, "config", "core.hooksPath"],
                        capture_output=True, text=True).stdout.strip()
    if hp:
        hp_abs = hp if os.path.isabs(hp) else os.path.join(repo_root, hp)
        if os.path.basename(os.path.normpath(hp_abs)) == "_":  # husky v9 managed dir
            return os.path.join(os.path.dirname(os.path.normpath(hp_abs)), "pre-commit"), "husky"
        return os.path.join(hp_abs, "pre-commit"), "hooksPath"
    return os.path.join(repo_root, ".git", "hooks", "pre-commit"), "git"


def is_tracked(repo_root, path):
    """True iff git ACTUALLY tracks `path` (a committed file) — NOT gitignored/untracked. The husky 'commit it'
    hint is honest only when the authored hook is tracked; some repos gitignore `.husky/`, making the hook LOCAL
    (no commit needed). Mechanism==husky alone does NOT imply tracked — telling the user to commit a gitignored
    file is a false instruction. Pure git query, no write."""
    rel = os.path.relpath(path, repo_root)
    r = subprocess.run(["git", "-C", repo_root, "ls-files", "--error-unmatch", rel],
                       capture_output=True, text=True)
    return r.returncode == 0


def strip_block(text):
    """Remove an existing BEGIN..END span (inclusive) — supports replace-in-place + uninstall. Idempotent."""
    out, skipping = [], False
    for ln in text.splitlines(keepends=True):
        if ln.startswith(BEGIN):
            skipping = True
            continue
        if skipping:
            if ln.startswith(END):
                skipping = False
            continue
        out.append(ln)
    return "".join(out)


def apply(repo_root, report_only=True, uninstall=False):
    """Write the target hook with the block replaced/removed. Returns (target_path, mechanism, action)."""
    target, mech = hook_target(repo_root)
    os.makedirs(os.path.dirname(target), exist_ok=True)
    existing = open(target, encoding="utf-8").read() if os.path.exists(target) else ""
    body = strip_block(existing)
    if uninstall:
        new, action = body, "uninstalled"
    else:
        if not body.startswith("#!"):  # fresh or shebang-less hook → give it a POSIX-sh shebang
            body = "#!/usr/bin/env sh\n" + (body if body.strip() else "")
        if body and not body.endswith("\n"):
            body += "\n"
        new = body + block(report_only)
        action = "installed" if BEGIN not in existing else "updated"
    with open(target, "w", encoding="utf-8") as fh:
        fh.write(new)
    os.chmod(target, os.stat(target).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
    return target, mech, action


def main():
    ap = argparse.ArgumentParser(description="chain the prevent gate into a repo's pre-commit hook (no clobber)")
    ap.add_argument("--repo", required=True, help="path inside the target git repo")
    ap.add_argument("--enforce", action="store_true", help="install in ENFORCE mode (blocks); default report-only")
    ap.add_argument("--uninstall", action="store_true", help="strip the prevent block, leave the hook intact")
    a = ap.parse_args()
    repo_root = subprocess.run(["git", "-C", a.repo, "rev-parse", "--show-toplevel"],
                               capture_output=True, text=True).stdout.strip()
    if not repo_root:
        print(f"install: {a.repo!r} is not inside a git repo", file=sys.stderr)
        return 2
    target, mech, action = apply(repo_root, report_only=not a.enforce, uninstall=a.uninstall)
    committed = ""
    if mech == "husky" and not a.uninstall:  # husky authored hook MAY be committed — but only if git tracks it
        committed = (" [TRACKED file — commit it in the target repo]" if is_tracked(repo_root, target)
                     else " [.husky gitignored here — local hook, nothing to commit]")
    mode = "" if a.uninstall else f" ({'enforce' if a.enforce else 'report-only'})"
    print(f"prevent install: {action}{mode} -> {target} [{mech}]{committed}")
    return 0


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