#!/usr/bin/env python3
"""Fail the commit if its message breaks the 50/72 rule.

Subject line <= 50 chars, a blank line, then body wrapped at <= 72 chars.
The subject also has to open with a module prefix. Wired in as a
prek/pre-commit commit-msg hook so it is enforced mechanically instead of by
memory.
"""

import re
import sys
from collections.abc import Sequence
from pathlib import Path

MAX_SUBJECT = 50
MAX_BODY = 72

# `module-prefix: description`, the prefix at the very start of the subject.
# A prefix names a file, so it spells the name as the file does: underscores
# within a name, `/` between folders, `+` between two modules.
SUBJECT_RE = re.compile(r"^[a-z0-9][a-z0-9_+./]*: \S")

# Subjects git or a rebase writes itself, which the rule is not about.
EXEMPT_PREFIXES = ("Merge ", "Revert ", "fixup! ", "squash! ", "amend! ")


def main(argv: Sequence[str]) -> int:
    if len(argv) != 2:
        sys.stderr.write(f"Usage: {argv[0]} COMMIT_MSG_FILE\n")
        return 2

    message_path = Path(argv[1])
    with message_path.open(encoding="utf-8") as handle:
        raw_lines = handle.read().splitlines()

    # `git commit -v` appends a diff below a scissors line; ignore it.
    cut = next(
        (
            i
            for i, line in enumerate(raw_lines)
            if ">8" in line and line[:1] == "#"
        ),
        None,
    )
    if cut is not None:
        raw_lines = raw_lines[:cut]

    lines = [line for line in raw_lines if not line.startswith("#")]
    while lines and not lines[-1].strip():
        lines.pop()
    if not lines:
        return 0

    errors: list[str] = []
    subject = lines[0]
    if len(subject) > MAX_SUBJECT:
        errors.append(f"subject is {len(subject)} chars (max {MAX_SUBJECT})")
    if not subject.startswith(EXEMPT_PREFIXES) and not SUBJECT_RE.match(
        subject
    ):
        head = subject.split(":")[0]
        if "," in head:
            errors.append(
                f"prefix `{head}` separates modules with a comma; join them"
                " with `+`"
            )
        elif "-" in head:
            errors.append(
                f"prefix `{head}` uses a dash; spell the module as its file"
                f" does (`{head.replace('-', '_')}`)"
            )
        else:
            errors.append(
                "subject must open with a lowercase module prefix, as in"
                " `camera: keep the target on Lara`"
            )
    if len(lines) > 1 and lines[1].strip():
        errors.append("second line must be blank (subject, blank, body)")
    for number, line in enumerate(lines[2:], start=3):
        if len(line) > MAX_BODY:
            errors.append(
                f"body line {number} is {len(line)} chars (max {MAX_BODY})"
            )

    if errors:
        sys.stderr.write("Commit message breaks the 50/72 rule:\n")
        for error in errors:
            sys.stderr.write(f"  - {error}\n")
        return 1
    return 0


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