#!/usr/bin/env python3
# File generated from our OpenAPI spec by Castiron. See CONTRIBUTING.md for details.
# Regenerate with Castiron; do not edit this file by hand.
"""Standalone, vendorable Castiron generated-content hashing and reporting.

Hash v1: SHA256(domain || u64(entry count) || entries), where each entry is
u64(path length) || path bytes || six ASCII Git mode bytes || u64(blob length)
|| blob bytes. Integers are unsigned big-endian. Entries sort by raw Git path.
Only .github/actions/ and .github/workflows/ are excluded. No SDK imports.
"""

from __future__ import annotations

import argparse
import base64
import collections
import hashlib
import html
import json
import os
import re
import struct
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, cast

DOMAIN = b"castiron-codegen-v1\0"
MARKER = "<!-- castiron:custom-code-report:v1 -->"
RUN_MARKER = re.compile(r"<!-- castiron:run:v1:(\d+):(\d+) -->")
SHA = re.compile(r"[0-9a-f]{40}\Z")
HASH = re.compile(r"[0-9a-f]{64}\Z")
REPOSITORY = re.compile(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\Z")
EXCLUDED = (b".github/actions/", b".github/workflows/")
PUBLIC_BLOCKED_PATHS = {
    b".castiron",
    b".github",
    b".gitmodules",
    b"CODEOWNERS",
    b"SECURITY.md",
    b"docs/CODEOWNERS",
    b"docs/SECURITY.md",
}
LABELS = {
    "newly_customized": "Newly customized",
    "removed": "Customization removed",
    "existing_changed": "Existing customization changed",
    "baseline_changed": "Generated baseline changed",
    "unchanged": "Existing customization unchanged",
    "no_longer_generated": "No longer generated",
    "newly_generation_owned": "Now generated; already existed",
}


class ReportError(ValueError):
    """The report cannot establish a trustworthy comparison."""


@dataclass(frozen=True)
class Entry:
    mode: bytes
    oid: str


def git(repo: Path, *args: str, input_bytes: bytes | None = None) -> bytes:
    env = os.environ.copy()
    # Preserve ordinary Git authentication/configuration, but do not inherit
    # repository routing from the caller (for example, a Git hook).
    for name in (
        "GIT_DIR",
        "GIT_COMMON_DIR",
        "GIT_WORK_TREE",
        "GIT_INDEX_FILE",
        "GIT_PREFIX",
        "GIT_OBJECT_DIRECTORY",
        "GIT_ALTERNATE_OBJECT_DIRECTORIES",
    ):
        env.pop(name, None)
    env["GIT_NO_REPLACE_OBJECTS"] = "1"
    result = subprocess.run(
        ["git", "--no-replace-objects", "-C", str(repo), *args],
        input=input_bytes,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        env=env,
        check=False,
    )
    if result.returncode:
        raise ReportError(f"git {args[0]} failed: {result.stderr.decode(errors='replace').strip()}")
    return result.stdout


def require_sha(value: str) -> str:
    if not SHA.fullmatch(value):
        raise ReportError("expected a full Git commit SHA")
    return value


def included(path: bytes) -> bool:
    return not path.startswith(EXCLUDED)


def tree_entries(repo: Path, commit: str) -> dict[bytes, Entry]:
    result: dict[bytes, Entry] = {}
    for record in git(repo, "ls-tree", "-r", "-z", require_sha(commit)).split(b"\0"):
        if not record:
            continue
        metadata, path = record.split(b"\t", 1)
        mode, kind, oid = metadata.split(b" ")
        if not included(path):
            continue
        if kind != b"blob" or mode not in {b"100644", b"100755", b"120000"}:
            raise ReportError(f"unsupported Git entry: {path!r}")
        result[path] = Entry(mode, oid.decode("ascii"))
    return result


def read_blobs(repo: Path, entries: dict[bytes, Entry]) -> dict[str, bytes]:
    oids = sorted({entry.oid for entry in entries.values()})
    output = git(
        repo, "cat-file", "--batch", input_bytes="".join(f"{oid}\n" for oid in oids).encode()
    )
    result: dict[str, bytes] = {}
    offset = 0
    for oid in oids:
        end = output.index(b"\n", offset)
        actual, kind, size = output[offset:end].split(b" ")
        if actual.decode() != oid or kind != b"blob":
            raise ReportError("unexpected Git object response")
        offset = end + 1
        length = int(size)
        result[oid] = output[offset : offset + length]
        offset += length
        if output[offset : offset + 1] != b"\n":
            raise ReportError("truncated Git object response")
        offset += 1
    if offset != len(output):
        raise ReportError("unexpected trailing Git object data")
    return result


def hash_codegen_commit(repo: Path, commit: str) -> str:
    entries = tree_entries(repo, commit)
    blobs = read_blobs(repo, entries)
    digest = hashlib.sha256(DOMAIN + struct.pack(">Q", len(entries)))
    for path, entry in sorted(entries.items()):
        content = blobs[entry.oid]
        digest.update(struct.pack(">Q", len(path)))
        digest.update(path)
        digest.update(entry.mode)
        digest.update(struct.pack(">Q", len(content)))
        digest.update(content)
    return digest.hexdigest()


def read_stats(repo: Path, commit: str) -> dict[str, str]:
    entry = git(repo, "ls-tree", require_sha(commit), "--", ".castiron.stats.yml")
    if not entry.startswith((b"100644 blob ", b"100755 blob ")):
        raise ReportError("Castiron stats must be a regular Git file")
    text = git(repo, "show", f"{require_sha(commit)}:.castiron.stats.yml").decode("utf-8")
    values: dict[str, str] = {}
    for line in text.splitlines():
        if not line.strip() or line.lstrip().startswith("#"):
            continue
        key, sep, value = line.partition(":")
        if not sep or key.strip() in values:
            raise ReportError("invalid or duplicate stats field")
        values[key.strip()] = value.strip().strip("\"'")
    if values.get("schema_version") != "1":
        raise ReportError("unsupported Castiron stats schema")
    require_sha(values.get("codegen_sha", ""))
    if not re.fullmatch(
        r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
        values.get("generation_id", ""),
    ):
        raise ReportError("invalid generation ID")
    if "codegen_hash" in values and not HASH.fullmatch(values["codegen_hash"]):
        raise ReportError("invalid codegen_hash")
    if "public_codegen_sha" in values:
        require_sha(values["public_codegen_sha"])
    return values


def public_metadata(repo: Path, commit: str) -> dict[str, str]:
    message = git(repo, "show", "-s", "--format=%B", require_sha(commit)).decode("utf-8")
    fields: dict[str, str] = {}
    for key in ("Version", "Generation-ID", "Hash", "Branch"):
        matches = re.findall(rf"^Castiron-Public-Codegen-{key}: (.+)$", message, re.MULTILINE)
        if len(matches) != 1:
            raise ReportError("invalid public codegen metadata")
        fields[key] = matches[0]
    if fields["Version"] != "1" or not HASH.fullmatch(fields["Hash"]):
        raise ReportError("unsupported public codegen metadata")
    if not fields["Branch"].startswith("codegen/"):
        raise ReportError("invalid public codegen branch")
    git(repo, "check-ref-format", f"refs/heads/{fields['Branch']}")
    return fields


def resolve_public_baseline(
    repo: Path, revision: str, *, fetch: bool, require_hash: bool, lineage: str | None = None
) -> dict[str, Any]:
    stats = read_stats(repo, revision)
    commit = stats.get("public_codegen_sha")
    if commit is None:
        # Bootstrap old public main without changing its schema-v1 stats. Search
        # only the already-verified public snapshot lineage supplied by PR head.
        if lineage is None:
            raise ReportError("stats do not contain public_codegen_sha")
        for candidate in git(repo, "rev-list", require_sha(lineage)).decode().splitlines():
            if public_metadata(repo, candidate)["Generation-ID"] == stats["generation_id"]:
                commit = candidate
                break
        if commit is None:
            raise ReportError("public snapshot lineage does not contain the base generation")
    elif fetch:
        git(repo, "fetch", "--quiet", "--no-tags", "origin", commit)
    metadata = public_metadata(repo, commit)
    if metadata["Generation-ID"] != stats["generation_id"]:
        raise ReportError("public codegen generation ID does not match stats")
    ref = f"refs/heads/{metadata['Branch']}"
    tracking = f"refs/castiron/report/{hashlib.sha256(ref.encode()).hexdigest()}"
    if fetch:
        git(repo, "fetch", "--quiet", "--no-tags", "origin", f"+{ref}:{tracking}")
    else:
        tracking = f"refs/remotes/origin/{metadata['Branch']}"
    git(repo, "merge-base", "--is-ancestor", commit, tracking)
    actual = hash_codegen_commit(repo, commit)
    if actual != metadata["Hash"]:
        raise ReportError("public codegen content does not match snapshot metadata")
    expected = stats.get("codegen_hash")
    if expected is None and require_hash:
        raise ReportError("head stats do not contain codegen_hash")
    if expected is not None and expected != actual:
        raise ReportError(f"codegen_hash mismatch: expected {expected}, got {actual}")
    return {
        "commit": commit,
        "generation_id": stats["generation_id"],
        "hash": actual,
        "hash_recorded": expected is not None,
    }


def public_snapshot_entries(
    source: Path,
    commit: str,
    destination: Path,
    public_base: str,
    approved_tree: str | None = None,
) -> dict[bytes, Entry]:
    entries = tree_entries(source, commit)
    approved = tree_entries(destination, public_base)
    if approved_tree is not None:
        approved.update(tree_entries(destination, approved_tree))
    for path in entries:
        if path in PUBLIC_BLOCKED_PATHS or path.startswith((b".castiron/", b".github/")):
            raise ReportError(f"refusing to publish private or governance snapshot path: {path!r}")
        if path not in approved:
            raise ReportError(
                f"snapshot path is absent from the approved public SDK tree: {path!r}"
            )
    return entries


def copy_generated_tree(
    source: Path,
    commit: str,
    destination: Path,
    public_base: str,
    approved_tree: str | None = None,
) -> str:
    """Transfer approved blobs/trees only, never source commits or ancestors."""
    entries = public_snapshot_entries(source, commit, destination, public_base, approved_tree)
    for oid, blob in read_blobs(source, entries).items():
        actual = git(destination, "hash-object", "-w", "--stdin", input_bytes=blob).decode().strip()
        if actual != oid:
            raise ReportError("source and destination Git object formats differ")
    directories: dict[bytes, list[bytes]] = {b"": []}
    for path, entry in entries.items():
        parent, _, name = path.rpartition(b"/")
        directories.setdefault(parent, []).append(
            entry.mode + b" blob " + entry.oid.encode() + b"\t" + name + b"\0"
        )
        while parent:
            parent = parent.rpartition(b"/")[0]
            directories.setdefault(parent, [])
    for directory in sorted(directories, key=lambda p: (p.count(b"/"), len(p)), reverse=True):
        tree = git(
            destination, "mktree", "-z", input_bytes=b"".join(directories[directory])
        ).strip()
        if not directory:
            return tree.decode()
        parent, _, name = directory.rpartition(b"/")
        directories[parent].append(b"040000 tree " + tree + b"\t" + name + b"\0")
    raise ReportError("could not construct public generated tree")


def create_public_snapshot(
    destination: Path,
    tree: str,
    generation: str,
    content_hash: str,
    branch: str,
    parent: str | None,
) -> str:
    message = (
        "Castiron generated snapshot\n\n"
        "Castiron-Public-Codegen-Version: 1\n"
        f"Castiron-Public-Codegen-Generation-ID: {generation}\n"
        f"Castiron-Public-Codegen-Hash: {content_hash}\n"
        f"Castiron-Public-Codegen-Branch: {branch}\n"
    )
    parents = ["-p", require_sha(parent)] if parent is not None else []
    snapshot = (
        git(
            destination,
            "-c",
            "user.name=Castiron",
            "-c",
            "user.email=noreply@openai.com",
            "commit-tree",
            tree,
            *parents,
            "-m",
            message,
        )
        .decode()
        .strip()
    )
    require_sha(snapshot)
    if hash_codegen_commit(destination, snapshot) != content_hash:
        raise ReportError("public snapshot content hash differs from private checkpoint")
    return snapshot


def prepare_public_snapshot(
    source: Path,
    source_base: str,
    source_head: str,
    destination: Path,
    public_base: str,
    branch: str,
) -> dict[str, Any]:
    """Prepare public-only generated history; never push anything."""
    read_stats(source, source_base)
    after = read_stats(source, source_head)
    public = read_stats(destination, public_base)
    baseline = resolve_checkpoint(source, after, fetch=True, require_hash=True)
    codegen_branch = f"codegen/{branch}"
    ref = f"refs/heads/{codegen_branch}"
    git(destination, "check-ref-format", ref)
    remote = git(destination, "ls-remote", "--refs", "origin", ref).decode().splitlines()
    previous = require_sha(remote[0].split()[0]) if remote else ""
    if previous:
        git(destination, "fetch", "--quiet", "--no-tags", "origin", previous)
        metadata = public_metadata(destination, previous)
        # Stats pin the snapshot, not the SDK branch that currently uses it.
        # Another promotion may legitimately reuse this public commit; its
        # original branch remains the locator used by resolve_public_baseline.
        if hash_codegen_commit(destination, previous) != metadata["Hash"]:
            raise ReportError("existing public codegen branch has invalid provenance")

    base_snapshot = public.get("public_codegen_sha")
    if base_snapshot:
        base_snapshot = resolve_public_baseline(
            destination, public_base, fetch=True, require_hash=False
        )["commit"]
    elif previous:
        try:
            base_snapshot = resolve_public_baseline(
                destination, public_base, fetch=True, require_hash=False, lineage=previous
            )["commit"]
        except ReportError:
            base_snapshot = None
    if base_snapshot is None:
        old = resolve_checkpoint(source, public, fetch=True, require_hash=False)
        # Initial bootstrap is limited to paths already present on public main.
        tree = copy_generated_tree(source, old["commit"], destination, public_base)
        base_snapshot = create_public_snapshot(
            destination, tree, old["generation_id"], old["hash"], codegen_branch, None
        )

    # Normal promotion has already applied the reviewed patch to the public
    # index. Private HEAD can contain unchanged private-only paths, so it is
    # not an approval boundary for publication.
    approved_tree = git(destination, "write-tree").decode().strip()
    tree = copy_generated_tree(source, baseline["commit"], destination, public_base, approved_tree)
    parent = base_snapshot
    if previous:
        ancestors = git(destination, "rev-list", previous).decode().splitlines()
        if base_snapshot in ancestors:
            parent = previous
        metadata = public_metadata(destination, previous)
        if (
            metadata["Generation-ID"] == after["generation_id"]
            and metadata["Hash"] == baseline["hash"]
            and git(destination, "rev-parse", f"{previous}^{{tree}}").decode().strip() == tree
            and base_snapshot in ancestors
        ):
            snapshot = previous
        else:
            snapshot = create_public_snapshot(
                destination, tree, after["generation_id"], baseline["hash"], codegen_branch, parent
            )
    elif (
        public_metadata(destination, base_snapshot)["Generation-ID"] == after["generation_id"]
        and git(destination, "rev-parse", f"{base_snapshot}^{{tree}}").decode().strip() == tree
    ):
        snapshot = base_snapshot
    else:
        snapshot = create_public_snapshot(
            destination, tree, after["generation_id"], baseline["hash"], codegen_branch, parent
        )
    stats_path = destination / ".castiron.stats.yml"
    current = git(source, "show", f"{require_sha(source_head)}:.castiron.stats.yml").decode("utf-8")
    lines = [line for line in current.splitlines() if not line.startswith("public_codegen_sha:")]
    stats_path.write_text("\n".join(lines) + f"\npublic_codegen_sha: {snapshot}\n")
    git(destination, "add", "--", ".castiron.stats.yml")
    return {
        "commit": snapshot,
        "branch": codegen_branch,
        "previous": previous,
        "hash": baseline["hash"],
    }


def resolve_baseline(
    repo: Path, revision: str, *, fetch: bool, require_hash: bool
) -> dict[str, Any]:
    return resolve_checkpoint(
        repo, read_stats(repo, revision), fetch=fetch, require_hash=require_hash
    )


def resolve_checkpoint(
    repo: Path, stats: dict[str, str], *, fetch: bool, require_hash: bool
) -> dict[str, Any]:
    commit = stats["codegen_sha"]
    if fetch:
        git(repo, "fetch", "--quiet", "--no-tags", "origin", commit)
    message = git(repo, "show", "-s", "--format=%B", commit).decode("utf-8")
    match = re.search(r"^Generation metadata: ([A-Za-z0-9+/=]+)$", message, re.MULTILINE)
    if not match:
        raise ReportError("codegen checkpoint has no generation metadata")
    try:
        decoded: object = json.loads(base64.b64decode(match[1], validate=True))
    except (ValueError, TypeError) as exc:
        raise ReportError("invalid codegen checkpoint metadata") from exc
    if not isinstance(decoded, dict):
        raise ReportError("invalid codegen checkpoint metadata")
    metadata = cast(dict[str, Any], decoded)
    if metadata.get("generation_id") != stats["generation_id"]:
        raise ReportError("codegen checkpoint generation ID does not match stats")
    branch = metadata.get("source_branch")
    if not isinstance(branch, str) or not branch:
        raise ReportError("codegen checkpoint has no source branch")
    ref = f"refs/heads/codegen/{branch}"
    git(repo, "check-ref-format", ref)
    tracking = f"refs/castiron/report/{hashlib.sha256(ref.encode()).hexdigest()}"
    if fetch:
        git(repo, "fetch", "--quiet", "--no-tags", "origin", f"+{ref}:{tracking}")
    else:
        tracking = f"refs/remotes/origin/codegen/{branch}"
    git(repo, "merge-base", "--is-ancestor", commit, tracking)
    actual = hash_codegen_commit(repo, commit)
    expected = stats.get("codegen_hash")
    if expected is None and require_hash:
        raise ReportError("head stats do not contain codegen_hash")
    if expected is not None and actual != expected:
        raise ReportError(f"codegen_hash mismatch: expected {expected}, got {actual}")
    return {
        "commit": commit,
        "generation_id": stats["generation_id"],
        "hash": actual,
        "hash_recorded": expected is not None,
    }


def numstats(repo: Path, generated: str, integrated: str) -> dict[bytes, tuple[str, str]]:
    result: dict[bytes, tuple[str, str]] = {}
    output = git(
        repo,
        "diff",
        "--numstat",
        "-z",
        "--no-renames",
        "--no-ext-diff",
        "--no-textconv",
        generated,
        integrated,
        "--",
    )
    for record in output.split(b"\0"):
        if record:
            added, removed, path = record.split(b"\t", 2)
            result[path] = (added.decode("ascii"), removed.decode("ascii"))
    return result


def classify(g0: Entry | None, i0: Entry | None, g1: Entry | None, i1: Entry | None) -> str | None:
    before = g0 is not None and g0 != i0
    after = g1 is not None and g1 != i1
    if g0 is not None and g1 is None and (before or i1 is not None):
        return "no_longer_generated"
    if g0 is None and g1 is not None and i0 is not None:
        return "newly_generation_owned"
    if not before and not after:
        return None
    if g0 is not None and g1 is not None and g0 != g1:
        return "baseline_changed"
    if not before:
        return "newly_customized"
    if not after:
        return "removed"
    return "unchanged" if i0 == i1 else "existing_changed"


def build_report(
    repo: Path,
    base: str,
    head: str,
    *,
    fetch: bool = False,
    require_head_hash: bool = False,
    public: bool = False,
) -> tuple[dict[str, Any], bytes]:
    base, head = require_sha(base), require_sha(head)
    comparison_base = git(repo, "merge-base", base, head).decode().strip()
    if public:
        after = resolve_public_baseline(repo, head, fetch=fetch, require_hash=require_head_hash)
        before = resolve_public_baseline(
            repo, comparison_base, fetch=fetch, require_hash=False, lineage=after["commit"]
        )
    else:
        before = resolve_baseline(repo, comparison_base, fetch=fetch, require_hash=False)
        after = resolve_baseline(repo, head, fetch=fetch, require_hash=require_head_hash)
    g0, i0 = tree_entries(repo, before["commit"]), tree_entries(repo, comparison_base)
    g1, i1 = tree_entries(repo, after["commit"]), tree_entries(repo, head)
    stats = numstats(repo, after["commit"], head)
    files: list[dict[str, Any]] = []
    current_paths: list[str] = []
    for path in sorted(g0.keys() | g1.keys()):
        category = classify(g0.get(path), i0.get(path), g1.get(path), i1.get(path))
        if category is None:
            continue
        custom_before = path in g0 and g0[path] != i0.get(path)
        custom_after = path in g1 and g1[path] != i1.get(path)
        added, removed = stats.get(path, ("0", "0")) if custom_after else ("0", "0")
        files.append(
            {
                "path": path.decode("utf-8"),
                "category": category,
                "custom_before": custom_before,
                "custom_after": custom_after,
                "added": added,
                "removed": removed,
            }
        )
        if custom_after:
            current_paths.append(path.decode("utf-8"))
    patch = b""
    if current_paths:
        patch = git(
            repo,
            "--literal-pathspecs",
            "diff",
            "--binary",
            "--full-index",
            "--no-renames",
            "--no-ext-diff",
            "--no-textconv",
            "--no-color",
            after["commit"],
            head,
            "--",
            *current_paths,
        )
    return {
        "schema_version": 1,
        "status": "ok",
        "public": public,
        "target_base_sha": base,
        "base_sha": comparison_base,
        "head_sha": head,
        "before": before,
        "after": after,
        "files": files,
        "counts": {
            "before": sum(f["custom_before"] for f in files),
            "after": sum(f["custom_after"] for f in files),
            **dict(collections.Counter(f["category"] for f in files)),
        },
    }, patch


def render_report(
    report: dict[str, Any],
    run_url: str = "",
    *,
    repository: str = "",
    run_id: int = 0,
    run_attempt: int = 0,
) -> str:
    head = require_sha(report["head_sha"])
    lines = [MARKER, "", "## Castiron custom code", ""]
    if report.get("status") != "ok":
        reason = html.escape(str(report.get("error", "Report could not be computed")))
        lines.extend([f"⚠️ Report unavailable for `{head[:12]}`.", "", reason])
    else:
        files = report["files"]
        counts: collections.Counter[str] = collections.Counter()
        for file in files:
            if file["category"] not in LABELS:
                raise ReportError("unknown report category")
            counts[file["category"]] += 1
        before = sum(bool(file["custom_before"]) for file in files)
        after = sum(bool(file["custom_after"]) for file in files)
        base = require_sha(report["base_sha"])
        newly_customized = sum(
            bool(file["custom_after"]) and not file["custom_before"] for file in files
        )
        removed = sum(
            bool(file["custom_before"])
            and not file["custom_after"]
            and file["category"] != "no_longer_generated"
            for file in files
        )
        if newly_customized == 0:
            remaining = (
                f"{after} mixed file{'s' if after != 1 else ''} remain{'s' if after == 1 else ''}"
            )
            changed = counts["existing_changed"]
            details = [f"{changed} existing customization{'s' if changed != 1 else ''} changed"]
            if removed:
                details.append(f"{removed} customizations removed")
            if counts["baseline_changed"]:
                details.append(f"{counts['baseline_changed']} generated baselines changed")
            lines.extend(
                [
                    "✅ No new custom-code files detected.",
                    "",
                    remaining + "; " + "; ".join(details) + ".",
                ]
            )
        else:
            lines.extend(
                [
                    f"**Mixed files: {before} → {after}**",
                    "",
                    (
                        f"{newly_customized} newly customized · {removed} customizations removed · "
                        f"{counts['existing_changed']} existing customizations changed · {counts['baseline_changed']} generated baselines changed"
                    ),
                ]
            )
        lines.extend(["", f"Compared `{base[:12]}` → `{head[:12]}`. Generated baselines verified."])
        if not report["before"]["hash_recorded"] or not report["after"]["hash_recorded"]:
            lines.extend(
                [
                    "",
                    "Legacy baseline: content hash computed from the pinned checkpoint; not yet recorded in that revision’s stats.",
                ]
            )
        interesting = [file for file in files if file["category"] != "unchanged"]
        if interesting:
            lines.extend(["", "| File | Result | Current custom patch |", "| --- | --- | --- |"])
            for file in interesting[:40]:
                path = (
                    html.escape(str(file["path"]))
                    .replace("|", "&#124;")
                    .replace("\n", "&#10;")
                    .replace("\r", "&#13;")
                )
                added, removed = str(file["added"]), str(file["removed"])
                if not re.fullmatch(r"\d+|-", added) or not re.fullmatch(r"\d+|-", removed):
                    raise ReportError("invalid line counts")
                delta = f"+{added} / −{removed}" if file["custom_after"] else "None"
                lines.append(f"| <code>{path}</code> | {LABELS[file['category']]} | {delta} |")
            if len(interesting) > 40:
                lines.extend(
                    [
                        "",
                        f"{len(interesting) - 40} more affected files are listed in the full report.",
                    ]
                )
        if counts["unchanged"]:
            lines.extend(
                [
                    "",
                    f"<details><summary>{counts['unchanged']} existing customizations unchanged</summary>",
                    "",
                ]
            )
            for file in [file for file in files if file["category"] == "unchanged"][:40]:
                lines.append(f"- <code>{html.escape(str(file['path']))}</code>")
            if counts["unchanged"] > 40:
                lines.extend(["", f"{counts['unchanged'] - 40} more in the full report."])
            lines.extend(["", "</details>"])
        lines.extend(
            [
                "",
                "A changed generated baseline means this report cannot reliably identify which handwritten lines changed.",
            ]
        )
        target_base = require_sha(report["target_base_sha"])
        lines.extend(["", "<details><summary>Inspect the custom-code diff</summary>", ""])
        if repository:
            if not REPOSITORY.fullmatch(repository) or min(run_id, run_attempt) <= 0:
                raise ReportError("invalid artifact download target")
            artifact = f"castiron-custom-code-{run_id}-{run_attempt}"
            lines.extend(
                [
                    "Download the exact patch produced by this run (requires repository access):",
                    "",
                    "```sh",
                    f"gh run download {run_id} --repo {repository} \\",
                    f"  --name {artifact} --dir /tmp/{artifact}",
                    f"git apply --stat /tmp/{artifact}/custom-code.patch",
                    f"cat /tmp/{artifact}/custom-code.patch",
                    "```",
                    "",
                ]
            )
        lines.extend(
            [
                "Or reproduce it from an SDK checkout containing the vendored reporter:",
                "",
                "```sh",
                f"git fetch --no-tags origin {target_base} {head}",
                "python3 scripts/castiron/custom_code_report.py report \\",
                f"  --base {target_base} \\",
                f"  --head {head} --fetch --require-head-hash{' --public' if report.get('public') else ''} \\",
                f"  --out /tmp/castiron-custom-code-{head[:12]}",
                f"cat /tmp/castiron-custom-code-{head[:12]}/custom-code.patch",
                "```",
                "",
                "This is the **current full custom patch** for mixed files, not an attribution of only the handwritten lines changed by this PR.",
                "",
                "</details>",
            ]
        )
    if run_url:
        lines.extend(["", f"[Full report and patch]({run_url})"])
    return "\n".join(lines) + "\n"


def api(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
    token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
    if not token:
        raise ReportError("GitHub token is required for comment publication")
    args = ["gh", "api", "--hostname", "github.com", "--method", method, path]
    if payload is not None:
        args.extend(["--input", "-"])
    result = subprocess.run(
        args,
        input=json.dumps(payload).encode() if payload is not None else None,
        capture_output=True,
        check=False,
        timeout=60,
    )
    if result.returncode:
        raise ReportError(
            f"GitHub API request failed: {result.stderr.decode(errors='replace').strip()}"
        )
    return json.loads(result.stdout)


def publish_comment(
    report: dict[str, Any],
    repository: str,
    number: int,
    run_id: int,
    run_attempt: int,
    *,
    artifact_run_id: int = 0,
    artifact_run_attempt: int = 0,
) -> str:
    if not REPOSITORY.fullmatch(repository) or min(number, run_id, run_attempt) <= 0:
        raise ReportError("invalid GitHub publication target")
    root = f"repos/{repository}"
    pull = api("GET", f"{root}/pulls/{number}")
    if (
        pull["state"] != "open"
        or pull["head"]["sha"] != report["head_sha"]
        or pull["base"]["sha"] != report["target_base_sha"]
    ):
        return "Skipped stale report"
    run = api("GET", f"{root}/actions/runs/{run_id}")
    if (
        run["event"] != "pull_request"
        or run.get("path", "").split("@", 1)[0] != ".github/workflows/castiron-custom-code.yml"
        or run["head_sha"] != report["head_sha"]
    ):
        raise ReportError("workflow run does not match report PR/head")
    associated = run["pull_requests"]
    if not associated:
        # GitHub can omit the PR association on workflow runs from forks.
        associated = api("GET", f"{root}/commits/{require_sha(run['head_sha'])}/pulls?per_page=100")
    if not any(pr["number"] == number for pr in associated):
        raise ReportError("workflow run does not match report PR/head")
    if run["run_attempt"] != run_attempt:
        return "Skipped stale report"
    artifact_run_id = artifact_run_id or run_id
    artifact_run_attempt = artifact_run_attempt or run_attempt
    body = render_report(
        report,
        f"https://github.com/{repository}/actions/runs/{artifact_run_id}",
        repository=repository,
        run_id=artifact_run_id,
        run_attempt=artifact_run_attempt,
    )
    body += f"\n<!-- castiron:run:v1:{run_id}:{run_attempt} -->\n"
    found = None
    for page in range(1, 101):
        comments = api("GET", f"{root}/issues/{number}/comments?per_page=100&page={page}")
        for comment in comments:
            if comment["user"]["login"] == "github-actions[bot]" and comment["body"].startswith(
                MARKER
            ):
                found = comment
                break
        if found is not None or len(comments) < 100:
            break
    if found is not None:
        previous = RUN_MARKER.search(found["body"])
        if previous and tuple(map(int, previous.groups())) > (run_id, run_attempt):
            return "Skipped stale report"
        if found["body"] == body:
            return str(found["html_url"])
    # The workflow serializes publishers; recheck after pagination before writing.
    pull = api("GET", f"{root}/pulls/{number}")
    if (
        pull["state"] != "open"
        or pull["head"]["sha"] != report["head_sha"]
        or pull["base"]["sha"] != report["target_base_sha"]
    ):
        return "Skipped stale report"
    if found is not None:
        result = api("PATCH", f"{root}/issues/comments/{found['id']}", {"body": body})
    else:
        result = api("POST", f"{root}/issues/{number}/comments", {"body": body})
    return str(result["html_url"])


def write_report(
    repo: Path,
    base: str,
    head: str,
    out: Path,
    *,
    fetch: bool,
    require_head_hash: bool,
    public: bool,
) -> dict[str, Any]:
    out.mkdir(parents=True, exist_ok=True)
    try:
        report, patch = build_report(
            repo, base, head, fetch=fetch, require_head_hash=require_head_hash, public=public
        )
    except (ReportError, UnicodeError, KeyError, ValueError) as exc:
        report = {
            "schema_version": 1,
            "status": "error",
            "target_base_sha": require_sha(base),
            "head_sha": require_sha(head),
            "error": str(exc),
        }
        patch = b""
    (out / "report.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
    (out / "custom-code.patch").write_bytes(patch)
    summary = render_report(report)
    (out / "summary.md").write_text(summary)
    sys.stdout.write(summary)
    return report


def trusted_report(
    repo: Path, repository: str, run_id: int, run_attempt: int, out: Path
) -> None:
    """Recompute from GitHub-associated Git objects, never from PR-produced artifacts."""
    if not REPOSITORY.fullmatch(repository) or min(run_id, run_attempt) <= 0:
        raise ReportError("invalid GitHub report target")
    root = f"repos/{repository}"
    run = api("GET", f"{root}/actions/runs/{run_id}")
    if (
        run["event"] != "pull_request"
        or run.get("path", "").split("@", 1)[0] != ".github/workflows/castiron-custom-code.yml"
        or run["status"] != "completed"
    ):
        raise ReportError("unexpected source workflow run")
    if run["run_attempt"] != run_attempt:
        return
    head = require_sha(run["head_sha"])
    associated = run["pull_requests"] or api("GET", f"{root}/commits/{head}/pulls?per_page=100")
    current: list[tuple[int, str]] = []
    for number in sorted({int(pr["number"]) for pr in associated}):
        if number <= 0:
            raise ReportError("invalid associated pull request")
        pull = api("GET", f"{root}/pulls/{number}")
        if (
            pull["state"] == "open"
            and pull["head"]["sha"] == head
            and pull["base"]["repo"]["full_name"] == repository
        ):
            current.append((number, require_sha(pull["base"]["sha"])))
    if not current:
        return
    if len(current) != 1:
        raise ReportError("workflow run has multiple current pull requests")
    number, base = current[0]
    public = not api("GET", root)["private"]
    # This must be a new, bare repository: no PR worktree, hooks, configuration,
    # submodules, or Python imports can affect the trusted reporter.
    repo.mkdir()
    git(repo, "init", "--quiet", "--bare")
    git(repo, "remote", "add", "origin", f"https://github.com/{repository}.git")
    git(repo, "fetch", "--quiet", "--no-tags", "origin", base, head)
    write_report(repo, base, head, out, fetch=True, require_head_hash=True, public=public)
    (out / "context.json").write_text(
        json.dumps(
            {"pr": number, "repository": repository, "run": run_id, "attempt": run_attempt}
        )
        + "\n"
    )


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    commands = parser.add_subparsers(dest="command", required=True)
    hashing = commands.add_parser("hash")
    hashing.add_argument("--repo", type=Path, default=Path.cwd())
    hashing.add_argument("--commit", required=True)
    reporting = commands.add_parser("report")
    reporting.add_argument("--repo", type=Path, default=Path.cwd())
    reporting.add_argument("--base", required=True)
    reporting.add_argument("--head", required=True)
    reporting.add_argument("--out", type=Path, required=True)
    reporting.add_argument("--fetch", action="store_true")
    reporting.add_argument("--require-head-hash", action="store_true")
    reporting.add_argument("--public", action="store_true")
    trusted = commands.add_parser("trusted-report")
    trusted.add_argument("--repo", type=Path, required=True)
    trusted.add_argument("--repository", required=True)
    trusted.add_argument("--run-id", type=int, required=True)
    trusted.add_argument("--run-attempt", type=int, required=True)
    trusted.add_argument("--out", type=Path, required=True)
    preparing = commands.add_parser("prepare-public")
    preparing.add_argument("--source-repo", type=Path, required=True)
    preparing.add_argument("--source-base", required=True)
    preparing.add_argument("--source-head", required=True)
    preparing.add_argument("--public-repo", type=Path, required=True)
    preparing.add_argument("--public-base", required=True)
    preparing.add_argument("--branch", required=True)
    commenting = commands.add_parser("comment")
    commenting.add_argument("--report", type=Path, required=True)
    commenting.add_argument("--repository", required=True)
    commenting.add_argument("--pr", type=int, required=True)
    commenting.add_argument("--run-id", type=int, required=True)
    commenting.add_argument("--run-attempt", type=int, required=True)
    commenting.add_argument("--artifact-run-id", type=int, default=0)
    commenting.add_argument("--artifact-run-attempt", type=int, default=0)
    args = parser.parse_args()
    try:
        if args.command == "hash":
            sys.stdout.write(hash_codegen_commit(args.repo, args.commit) + "\n")
        elif args.command == "prepare-public":
            sys.stdout.write(
                json.dumps(
                    prepare_public_snapshot(
                        args.source_repo,
                        args.source_base,
                        args.source_head,
                        args.public_repo,
                        args.public_base,
                        args.branch,
                    )
                )
                + "\n"
            )
        elif args.command == "trusted-report":
            trusted_report(args.repo, args.repository, args.run_id, args.run_attempt, args.out)
        elif args.command == "comment":
            if args.report.stat().st_size > 5_000_000:
                raise ReportError("report artifact is too large")
            report = json.loads(args.report.read_text())
            sys.stdout.write(
                publish_comment(
                    report,
                    args.repository,
                    args.pr,
                    args.run_id,
                    args.run_attempt,
                    artifact_run_id=args.artifact_run_id,
                    artifact_run_attempt=args.artifact_run_attempt,
                )
                + "\n"
            )
        else:
            report = write_report(
                args.repo,
                args.base,
                args.head,
                args.out,
                fetch=args.fetch,
                require_head_hash=args.require_head_hash,
                public=args.public,
            )
            return 0 if report["status"] == "ok" else 1
    except (ReportError, OSError, subprocess.TimeoutExpired) as exc:
        sys.stderr.write(f"Castiron custom-code report: {exc}\n")
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
