#!/usr/bin/env python3
"""Refuse changed thread READMEs with invalid frontmatter, heading or folder id.

Required fields are id, status and tense; reference requires settled status.
Keys must be unique ASCII names, values must have balanced quotes and lists,
and for must be a string. Only an empty line is skipped, and required lists
remain literal strings: these are the shell gate's verdicts, not a new grammar.
--file checks one file; an all-zero old tip checks every incoming README.
Git's hook environment, including GIT_DIR, is kept so bare hooks find the repo.
"""

from pathlib import Path
import subprocess
import sys


class Refusal(Exception):
    pass


def unquote(value):
    if value.startswith('"') and value.endswith('"') and len(value) >= 2:
        inner = value[1:-1]
        if '"' not in inner:
            return inner
    elif '"' not in value:
        return value
    raise Refusal(f"unclosed quote: {value}")


def frontmatter_line(line, values):
    if line == "":
        return
    key, separator, raw = line.partition(":")
    key = key.strip(" ")
    letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    allowed = letters + "0123456789_-"
    if (line.startswith((" ", "\t")) or not separator or not key
            or key[0] not in letters or any(char not in allowed for char in key)):
        raise Refusal(f"frontmatter line not key: value: {line}")
    if key in values:
        raise Refusal(f"duplicate key: {key}")
    raw = raw.strip(" ")
    if raw.startswith("["):
        if key == "for":
            raise Refusal("for must be a string")
        if not raw.endswith("]"):
            raise Refusal(f"list unclosed: {key}")
        inner = raw[1:-1].strip(" ")
        if inner:
            for part in inner.split(","):
                piece = part.strip(" ")
                if not piece:
                    raise Refusal(f"list malformed: {key}")
                unquote(piece)
        values[key] = raw  # Shell stores lists literally, even required fields.
    else:
        values[key] = unquote(raw)


def require_fields(values):
    for key, options in (("id", None), ("status", "open|live|dormant|settled|retired"),
                         ("tense", "plan|reference")):
        value = values.get(key, "")
        if not value:
            raise Refusal(f"{key} missing")
        if options and value not in options.split("|"):
            raise Refusal(f"{key} {value} is not one of {options}")
    if values["tense"] == "reference" and values["status"] != "settled":
        raise Refusal("tense reference requires status settled")


def check_content(content, folder):
    # The shell removes every CR and splits only on LF, even for CRLF input.
    lines = content.replace("\r", "").split("\n")
    if lines[-1] == "":
        lines.pop()
    if not lines or lines[0] != "---":
        raise Refusal("frontmatter missing")
    try:
        end = lines.index("---", 1)
    except ValueError:
        raise Refusal("frontmatter unclosed") from None
    values = {}
    for line in lines[1:end]:
        frontmatter_line(line, values)
    require_fields(values)
    heading = next((line for line in lines[end + 1:] if line != ""), None)
    if heading is None:
        raise Refusal("heading missing")
    rest = heading[2:]
    if not heading.startswith("# ") or rest.startswith("#") or not rest.strip():
        raise Refusal("heading must be a single '# ' line")
    if folder and values["id"] != folder:
        raise Refusal(f"id {values['id']} is not folder {folder}")


def check(display, content, folder):
    try:
        check_content(content, folder)
        return 0
    except Refusal as exc:
        print(f"{display}: {exc}", file=sys.stderr)
        return 1


def git(*args):
    return subprocess.run(["git", *args], capture_output=True)


def usage():
    print("usage: 10-readme <old> <new> | 10-readme --file PATH", file=sys.stderr)
    return 2


def main(args):
    if len(args) != 2:
        return usage()
    if args[0] == "--file":
        path = Path(args[1])
        if not path.is_file():
            print(f"{args[1]}: no such file", file=sys.stderr)
            return 1
        content = path.read_bytes().decode("utf-8", "surrogateescape")
        folder = (path.parent.name or str(path.parent)) if path.name == "README.md" else ""
        return check(args[1], content, folder)
    old, new = args
    if not old:
        return usage()
    if all(char == "0" for char in old):
        paths = git("ls-tree", "-r", "--name-only", new, "--", "threads")
    else:
        paths = git("diff", "--name-only", "--diff-filter=ACMR", old, new, "--", "threads")
    if paths.returncode:
        sys.stderr.buffer.write(paths.stderr)
        return paths.returncode
    status = 0
    for path in paths.stdout.decode("utf-8", "surrogateescape").split("\n"):
        parts = path.split("/")
        if len(parts) != 3 or parts[0] != "threads" or not parts[1] or parts[2] != "README.md":
            continue
        blob = git("show", f"{new}:{path}")
        if blob.returncode:
            print(f"{path}: unreadable at {new}", file=sys.stderr)
            status = 1
            continue
        status |= check(path, blob.stdout.decode("utf-8", "surrogateescape"), parts[1])
    return status


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