#!/usr/bin/env python3
"""Foundation's guard on pushes to a default branch (main or master).

git runs this before every push from the agent's sandbox: the image sets
core.hooksPath through GIT_CONFIG_COUNT/KEY/VALUE, which git treats like `-c`
and so ranks above a repository's own config. A push that updates
refs/heads/main or refs/heads/master is refused unless the instance's
`github.directPush` (FOUNDATION_DIRECT_PUSH, JSON, set by the agent from
config.yaml every turn) names this repository AND every file the push changes
sits under one of its paths. Deleting or creating main, or a push that is not a
fast-forward, is always refused. Pushes to any other branch pass. The
repository's own pre-push hook, if it has one, runs afterwards.

stdin, one line per ref: <local ref> <local sha> <remote ref> <remote sha>
argv: <remote name> <remote url>
"""

import json
import os
import re
import subprocess
import sys

DEFAULT_BRANCHES = ("refs/heads/main", "refs/heads/master")
REPO_FROM_URL = re.compile(r"^https://(?:[^@/]+@)?github\.com/([^/]+/[^/]+?)(?:\.git)?/?$")


def git(*args: str) -> str:
    return subprocess.run(["git", *args], capture_output=True, text=True, check=True).stdout


def is_zero(sha: str) -> bool:
    return set(sha) == {"0"}


def allowed_paths(repo: str) -> list[str]:
    try:
        rules = json.loads(os.environ.get("FOUNDATION_DIRECT_PUSH") or "[]")
    except ValueError:
        return []
    for rule in rules if isinstance(rules, list) else []:
        if isinstance(rule, dict) and str(rule.get("repo", "")).lower() == repo.lower():
            return [p for p in rule.get("paths", []) if isinstance(p, str) and p]
    return []


def under(path: str, allowed: list[str]) -> bool:
    return any(path.startswith(a) if a.endswith("/") else path == a for a in allowed)


def refuse(message: str) -> None:
    print(f"Foundation: {message}", file=sys.stderr)
    sys.exit(1)


def check(remote_url: str, lines: list[str]) -> None:
    match = REPO_FROM_URL.match(remote_url)
    repo = match.group(1) if match else ""
    for line in lines:
        parts = line.split()
        if len(parts) != 4:
            continue
        _local_ref, local_sha, remote_ref, remote_sha = parts
        if remote_ref not in DEFAULT_BRANCHES:
            continue
        branch = remote_ref.removeprefix("refs/heads/")
        allowed = allowed_paths(repo) if repo else []
        if not allowed:
            refuse(
                f"never push to {branch}. Push a branch and open a pull request"
                f" ({repo or remote_url} has no github.directPush exception)."
            )
        if is_zero(local_sha) or is_zero(remote_sha):
            refuse(f"deleting or creating {branch} is never allowed.")
        try:
            git("merge-base", "--is-ancestor", remote_sha, local_sha)
        except subprocess.CalledProcessError:
            refuse(f"{branch} moved or is not a fast-forward: pull, re-apply your change, push again.")
        changed = [p for p in git("diff", "--name-only", "-z", remote_sha, local_sha).split("\0") if p]
        outside = [p for p in changed if not under(p, allowed)]
        if outside:
            refuse(
                f"this push to {branch} changes files outside {', '.join(allowed)}: "
                f"{', '.join(outside[:5])}. Push a branch and open a pull request."
            )


def run_repository_hook(argv: list[str], stdin: str) -> int:
    """The repository's own pre-push hook: its core.hooksPath, else .git/hooks."""
    try:
        hooks = git("config", "--local", "--get", "core.hooksPath").strip()
    except subprocess.CalledProcessError:
        hooks = ""
    if hooks == "":
        hooks = os.path.join(git("rev-parse", "--git-common-dir").strip(), "hooks")
    elif not os.path.isabs(hooks):
        hooks = os.path.join(git("rev-parse", "--show-toplevel").strip(), hooks)
    hook = os.path.join(hooks, "pre-push")
    if os.path.realpath(hook) == os.path.realpath(__file__) or not os.access(hook, os.X_OK):
        return 0
    return subprocess.run([hook, *argv], input=stdin, text=True).returncode


def main() -> None:
    argv = sys.argv[1:]
    stdin = sys.stdin.read()
    check(argv[1] if len(argv) > 1 else "", stdin.splitlines())
    sys.exit(run_repository_hook(argv, stdin))


if __name__ == "__main__":
    main()
