#!/usr/bin/env python3
"""Guard the nomarchy-menu Back/Left contract (BACKLOG #106).

Every *internal* list picker built with `rofi_menu` must emit the shared
`↩ Back` row (via `back`, `printf … "$BACK"`, or a literal). External
modi/tools and free-text prompts are listed as exceptions — Esc is their
back path.

Usage: check-menu-back.py <modules/home/rofi.nix>
Pure — no rofi runtime. Fails on a list menu that can strand the user.
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

if len(sys.argv) != 2:
    sys.exit(f"usage: {sys.argv[0]} <rofi.nix>")

text = Path(sys.argv[1]).read_text(encoding="utf-8")

# Strip Nix multi-line comments lightly? Keep simple — work on the whole file.
# Find `| rofi_menu` call sites (possibly with line breaks before `|`).
# Look at a window of lines before each call for BACK emission.

lines = text.splitlines()

# Exception markers: if the window mentions these, allow no BACK row.
EXCEPTION_MARKERS = (
    "External tool:",
    "external modi",
    "Esc closes",
    "Esc is their back",
    "free-text",
    "Free-text",
    "no list to",
    "rofi_menu -p search",  # web: empty stdin free-text
    "rofi_menu -p ask",
    # calc/emoji/network are exec-only without rofi_menu list construction
)

def window_has_back(chunk: str) -> bool:
    # Shared helpers emit ↩ Back: bare `back`, printf of $BACK, or a literal.
    if re.search(r"(?m)^\s*back\s*$", chunk):
        return True
    if re.search(r"\bback\b", chunk) and "back()" not in chunk.split("rofi_menu")[0][-20:]:
        # `back` as a command (not only the word in comments). Prefer line-start.
        if re.search(r"(?m)^\s*back\b", chunk):
            return True
    if "$BACK" in chunk or r"\$BACK" in chunk or "↩ Back" in chunk:
        return True
    if re.search(r'printf[^\n]*BACK', chunk):
        return True
    return False


def window_is_exception(chunk: str) -> bool:
    return any(m in chunk for m in EXCEPTION_MARKERS)


# Match lines that invoke rofi_menu as a picker (not the function definition).
invoke_re = re.compile(r"\brofi_menu\b")
bad: list[str] = []
for i, line in enumerate(lines):
    if "rofi_menu()" in line or "rofi_menu ()" in line:
        continue
    if not invoke_re.search(line):
        continue
    # Skip comments and the definition of the helper's docs.
    stripped = line.lstrip()
    if stripped.startswith("#"):
        continue
    # Window: up to 40 lines before this call (menu construction).
    start = max(0, i - 40)
    chunk = "\n".join(lines[start : i + 1])
    if window_is_exception(chunk):
        continue
    # Free-text: empty stdin piped in
    if re.search(r"rofi_menu[^\n]*<\s*/dev/null", chunk) or re.search(
        r"rofi_menu[^\n]*\n[^\n]*<\s*/dev/null", chunk
    ):
        continue
    if not window_has_back(chunk):
        bad.append(f"line {i + 1}: rofi_menu without ↩ Back in preceding construction:\n  {line.strip()}")

if bad:
    print("check-menu-back: FAIL — internal list menus must end with ↩ Back:\n", file=sys.stderr)
    for b in bad:
        print(b, file=sys.stderr)
    sys.exit(1)

print(f"check-menu-back: ok — {sum(1 for l in lines if invoke_re.search(l) and 'rofi_menu()' not in l)} rofi_menu sites scanned")
