#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
from __future__ import annotations

import argparse
import ast
import hashlib
import json
import subprocess
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
OUTPUT = REPO_ROOT / "solstone/think/generated/access_rejections.py"
ORACLE_COMMIT = "dd04f55c8"
ORACLE_PATH = "solstone/think/sol_cli.py"
ORACLE_BLOB = "a20570fc0994f6215a013e8c89ce7776ddec7d17"
EXPECTED_COUNT = 5
SENTINELS = {"import", "call"}


def git_bytes(*args: str) -> bytes:
    return subprocess.check_output(["git", *args], cwd=REPO_ROOT)


def git_hash_object(data: bytes) -> str:
    header = f"blob {len(data)}\0".encode()
    return hashlib.sha1(header + data, usedforsecurity=False).hexdigest()


def oracle_text() -> str:
    blob = git_bytes("rev-parse", f"{ORACLE_COMMIT}:{ORACLE_PATH}").decode().strip()
    if blob != ORACLE_BLOB:
        raise RuntimeError(
            f"{ORACLE_COMMIT}:{ORACLE_PATH} is {blob}, expected {ORACLE_BLOB}"
        )
    data = git_bytes("show", f"{ORACLE_COMMIT}:{ORACLE_PATH}")
    digest = git_hash_object(data)
    if digest != ORACLE_BLOB:
        raise RuntimeError(f"extracted oracle blob is {digest}, expected {ORACLE_BLOB}")
    return data.decode()


def call_surface(node: ast.AST) -> str | None:
    if not isinstance(node, ast.Call) or len(node.args) < 2:
        return None
    surface = node.args[1]
    if isinstance(surface, ast.Constant) and isinstance(surface.value, str):
        return surface.value
    return None


def extract() -> list[str]:
    tree = ast.parse(oracle_text())
    commands: list[str] = []
    for node in tree.body:
        if isinstance(node, ast.Assign) and len(node.targets) == 1:
            target = node.targets[0]
            value = node.value
        elif isinstance(node, ast.AnnAssign):
            target = node.target
            value = node.value
        else:
            continue
        if not isinstance(target, ast.Name) or target.id != "COMMANDS":
            continue
        if not isinstance(value, ast.Dict):
            raise RuntimeError("oracle COMMANDS must be a dict")
        for key, item in zip(value.keys, value.values, strict=True):
            if not isinstance(key, ast.Constant) or not isinstance(key.value, str):
                continue
            if call_surface(item) == "access":
                commands.append(key.value)
    result = sorted(commands)
    if not result:
        raise RuntimeError("access rejection extraction is empty")
    missing = sorted(SENTINELS - set(result))
    if missing:
        raise RuntimeError(f"missing access-rejection sentinels: {missing}")
    if len(result) != EXPECTED_COUNT:
        raise RuntimeError(
            f"access rejection count {len(result)} != {EXPECTED_COUNT}: {result!r}"
        )
    return result


def render(commands: list[str]) -> str:
    lines = [
        "# SPDX-License-Identifier: AGPL-3.0-only",
        "# Copyright (c) 2026 sol pbc",
        "# Generated by scripts/build_journal_access_rejection_inventory.py.",
        "",
        "JOURNAL_ACCESS_ONLY_COMMANDS: tuple[str, ...] = (",
    ]
    lines.extend(f"    {json.dumps(command)}," for command in commands)
    lines.extend([")", ""])
    return "\n".join(lines)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Build journal access-only rejection inventory."
    )
    parser.add_argument("--output", type=Path, default=OUTPUT)
    parser.add_argument("--check", action="store_true")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    output = args.output.resolve()
    rendered = render(extract())
    if args.check:
        if not output.is_file():
            print(f"{output} is missing")
            return 1
        if output.read_text() != rendered:
            print(
                f"{output} is stale; run make build-journal-access-rejection-inventory"
            )
            return 1
        print(f"{output} is current")
        return 0
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(rendered)
    print(f"wrote {output}")
    return 0


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