#!/usr/bin/env python3
# coding=utf8

"""
(C) 2026 Jack Lloyd

Botan is released under the Simplified BSD License (see license.txt)
"""

import argparse
import os
import re
import subprocess
import sys

# Stanza must appear near the start of the file, and must include a date.
GENERATED_RE = re.compile(
    r'This file was automatically generated by\b.*?\bon (\d{4}-\d{2}-\d{2})',
    re.DOTALL)

# Only scan the head of the file; the stanza always lives in the opening
# comment block.
SCAN_BYTES = 512


def run_git(*args):
    return subprocess.check_output(['git'] + list(args), text=True)


def generation_date(content):
    """Return the YYYY-MM-DD stamp from the stanza, or None if no stanza."""
    m = GENERATED_RE.search(content[:SCAN_BYTES])
    return m.group(1) if m else None


def file_at_ref(ref, path):
    """Return the text contents of <ref>:<path>, or None if missing."""
    try:
        return subprocess.check_output(
            ['git', 'show', f'{ref}:{path}'],
            text=True, stderr=subprocess.DEVNULL,
            errors='replace')
    except subprocess.CalledProcessError:
        return None


def file_in_working_tree(path):
    try:
        with open(path, encoding='utf8', errors='replace') as f:
            return f.read(SCAN_BYTES)
    except OSError:
        return None


def changed_files(base):
    """Files changed between base...HEAD plus any uncommitted changes."""
    out = set()
    out.update(run_git('diff', '--name-only', f'{base}...HEAD').split())
    out.update(run_git('diff', '--name-only', 'HEAD').split())
    out.discard('')
    return sorted(out)


def default_base():
    # On GitHub Actions during a pull_request event, the target branch name
    # is in GITHUB_BASE_REF (e.g. "master"). The remote ref is "origin/<name>".
    github_base = os.environ.get('GITHUB_BASE_REF')
    if github_base:
        return f'origin/{github_base}'
    return 'origin/master'


def main(argv=None):
    parser = argparse.ArgumentParser(description="Check that generated files are not modified.")
    parser.add_argument(
        '--base', default=default_base(),
        help='git ref to diff against (default: %(default)s)')
    args = parser.parse_args(argv)

    try:
        run_git('rev-parse', '--verify', args.base)
    except subprocess.CalledProcessError:
        # If the base isn't present (shallow clone, running on master
        # itself, non-PR workflow, ...) there's nothing to compare against.
        print(f'skipping: base ref {args.base!r} not available', file=sys.stderr)
        return 0

    flagged = []
    for path in changed_files(args.base):
        print("Checking '%s'..." % (path))
        working = file_in_working_tree(path)
        if working is None:
            continue  # deleted

        new_date = generation_date(working)
        if new_date is None:
            continue  # not a generated file

        base_content = file_at_ref(args.base, path)
        if base_content is None:
            continue  # newly added file; nothing to compare against

        old_date = generation_date(base_content)
        if old_date is None:
            continue

        if old_date == new_date:
            flagged.append((path, old_date))

    if flagged:
        print('ERROR: generated file(s) were modified without updating the generation date.')
        print('These files are regenerated by a gen_*.py script; edit the script and regenerate')
        print('rather than editing the file directly.')
        print()
        for path, date in flagged:
            print(f'  {path}  (date unchanged: {date})')
        return 1

    return 0


if __name__ == '__main__':
    sys.exit(main())
