#!/usr/bin/env python3
"""AG-03 CI gate — fail if any commit on this branch lacks both required trailers.

Every commit MUST carry **both** ``Signed-off-by:`` and ``Co-Authored-By:``
trailers (AGENTS **AG-03**). The ``.gitmessage`` template + the
``prepare-commit-msg`` hook (AG-18) handle the *local* side, but those depend on
each contributor's git config; this is the **authoritative server-side gate** —
it runs in CI, depends on no local setup, and catches **both** trailers (the
hook only auto-fills the constant ``Signed-off-by``).

Run on ``push`` / ``pull_request``. It checks every non-merge commit unique to
the current branch relative to its base (``CI_COMMIT_TARGET_BRANCH`` or
``main``) and exits non-zero listing any commit missing a required trailer.

Stdlib-only. The pure :func:`missing_trailers` predicate is unit-tested; the
git plumbing around it is a thin shell.
"""

from __future__ import annotations

import os
import subprocess
import sys

# Trailer keys every commit MUST carry, compared case-insensitively (git
# trailers are conventionally case-insensitive: ``Co-authored-by`` == ``Co-Authored-By``).
REQUIRED_TRAILERS: tuple[str, ...] = ("signed-off-by", "co-authored-by")


def missing_trailers(trailers_block: str) -> list[str]:
    """Return the required trailer keys absent from a commit's trailer block.

    Args:
        trailers_block: the unfolded trailer lines of one commit (e.g. the
            output of ``git log -1 --format='%(trailers:only,unfold)'``).

    Returns:
        The subset of :data:`REQUIRED_TRAILERS` not present, in canonical order
        (empty when the commit carries both).
    """
    present = {
        line.split(":", 1)[0].strip().lower() for line in trailers_block.splitlines() if ":" in line
    }
    return [key for key in REQUIRED_TRAILERS if key not in present]


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


def _resolve_base() -> str:
    """The ref to diff against — the remote base branch if CI fetched it."""
    base_branch = os.environ.get("CI_COMMIT_TARGET_BRANCH") or "main"
    remote = f"origin/{base_branch}"
    try:
        subprocess.run(["git", "rev-parse", "--verify", remote], capture_output=True, check=True)
    except subprocess.CalledProcessError:
        return base_branch  # local fallback (e.g. running outside CI)
    return remote


def main() -> int:
    base = _resolve_base()
    try:
        shas = [
            s
            for s in _run(["git", "log", "--no-merges", "--format=%H", f"{base}..HEAD"]).split()
            if s
        ]
    except subprocess.CalledProcessError as exc:
        # Fail-open on range-computation quirks (shallow clone, missing base) —
        # the hook + template are the belt; a CI-clone edge case must not block.
        print(
            f"check-commit-trailers: cannot compute {base}..HEAD ({exc}); skipping.",
            file=sys.stderr,
        )
        return 0

    bad = [
        (sha, absent)
        for sha in shas
        if (
            absent := missing_trailers(
                _run(["git", "log", "-1", "--format=%(trailers:only,unfold)", sha])
            )
        )
    ]
    if bad:
        print("AG-03: commit(s) missing a required trailer:", file=sys.stderr)
        for sha, absent in bad:
            print(f"  {sha[:12]} — missing {', '.join(absent)}", file=sys.stderr)
        print("Every commit MUST carry Signed-off-by AND Co-Authored-By (AG-03).", file=sys.stderr)
        return 1

    print(f"AG-03: all {len(shas)} commit(s) on this branch carry both trailers.")
    return 0


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