#!/usr/bin/env python3
"""Refuse new commits with missing labels, unapproved taint or no self audience.

Unlabeled committers must be enrolled in the incumbent people-commit list
(the incoming list on the first push). Every Widening must name a clause
marker in an incoming agreement, even on clean or unlabeled commits.
A valid widening permits taint=1. Every commit is judged after a refusal.
These trailers stop accidents; anyone holding push credentials can forge them.
Git's hook environment, including GIT_DIR, is kept.
"""

import re
import subprocess
import sys


def git(*args, input=None):
    return subprocess.run(["git", *args], input=input, capture_output=True,
                          text=True, encoding="utf-8", errors="surrogateescape")


def zeros(value):
    return bool(value) and all(char == "0" for char in value)


def enrolled_people(revision):
    result = git("show", f"{revision}:core/loom/people-commit")
    return {line.strip() for line in result.stdout.split("\n")
            if line.strip() and not line.strip().startswith("#")}


def trailers(message):
    result = git("interpret-trailers", "--parse", input=message + "\n")
    values = {}
    for line in result.stdout.split("\n"):
        key, separator, value = line.partition(":")
        if separator:
            values.setdefault(key, []).append(value.lstrip())
    return values


def valid_widening(value, new):
    file, separator, clause = value.partition("#")
    parts = file.split("/")
    allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
    if (not separator or not clause or any(char not in allowed for char in clause)
            or len(parts) != 3 or parts[0] != "people" or parts[2] != "agreement.md"
            or not parts[1] or parts[1].startswith(".")):
        return False
    blob = git("show", f"{new}:{file}")
    return blob.returncode == 0 and "{#" + clause + "}" in blob.stdout


def main(args):
    if len(args) < 2 or not args[0] or not args[1]:
        print("usage: 20-labels <old> <new>", file=sys.stderr)
        return 2
    old, new = args[:2]
    people = enrolled_people(new if zeros(old) else old)
    revisions = new if zeros(old) else f"{old}..{new}"
    commits = git("rev-list", "--reverse", revisions)
    if commits.returncode:
        return 0  # The shell check has no commits to judge on a failed rev-list.
    failed = False
    for sha in commits.stdout.split("\n"):
        if not sha:
            continue
        def refuse(reason):
            nonlocal failed
            print(f"{sha[:12]}: {reason}", file=sys.stderr)
            failed = True

        email = git("log", "-1", "--format=%ce", sha).stdout.rstrip("\n")
        message = git("log", "-1", "--format=%B", sha).stdout.rstrip("\n")
        values = trailers(message)
        label = values.get("Loom-Label", [""])[0]
        strand = values.get("Loom-Strand", [""])[0] or "?"
        opened = False
        for cite in values.get("Widening", []):
            if not cite:
                continue
            if valid_widening(cite, new):
                opened = True
            else:
                refuse(f"widening {cite}")
        if not label:
            if email not in people:
                refuse("no Loom-Label")
            continue
        # Greedy prefixes preserve sed's last match: e.g. taint=0; taint=1.
        taint = re.search(r".*taint=([01])", label)
        audience = re.search(r".*audience=([^;]*)", label)
        if taint is None:
            refuse("Loom-Label unreadable")
            continue
        if taint[1] == "1" and not opened:
            refuse(f"taint=1 strand {strand}")
        if audience is None or "self" not in audience[1].split(","):
            refuse("audience missing self")
    return int(failed)


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