# -*- coding: utf-8 -*-
"""v0.126.0 shared app-row dedup gate — the app-list row markup exists ONCE.

The canonical row (icon + name left, action block right) is defined in
templates/app_row.html (app_list_row / app_list_row_end). Every list surface renders
THROUGH it; hand-rolled copies are the defect this gate extinguishes (the pre-0.126.0
state: three visually diverging row structures across four surfaces).

Asserts:
  1. The row-opening markup (`class="app-row"...`) appears in app_row.html ONLY.
  2. The row-icon markup (`app-row-icon`) appears only in app_row.html plus the ONE
     allowlisted aligned copy: the backups_apps.html expander header (it owns the
     expand/collapse toggle, so it is aligned to the grammar, not rendered through
     the partial).
  3. The old duplicated structures are gone from the converted surfaces:
     storage-path-item rows on the backups pages, stack-card rows on the dashboard.
  4. Each converted surface actually references the partial.

Run from controller/:  python scripts/app_row_dedup_gate.py
Exit 1 on any violation.
"""
import io, os, re, sys

TPL = os.path.join("internal", "web", "templates")

# (file, forbidden-pattern, why)
FORBIDDEN = [
    ("backups_remote.html", r"storage-path-item", "toggle list must render through app_list_row"),
    ("backups_remote.html", r"storage-path-header", "old row structure"),
    ("backups_restore.html", r"storage-path-item", ".fab + restore-to-verify lists must render through app_list_row"),
    ("backups_restore.html", r"storage-path-header", "old row structure"),
    ("dashboard.html", r"stack-card", "dashboard rows must render through app_list_row"),
    ("dashboard.html", r"stack-info", "old row structure"),
    ("dashboard.html", r'stack-logo"', "old row logo (stack-logo-lg on stacks.html is the card, not a list row)"),
]

# files that MUST reference the shared partial
MUST_USE = ["backups_remote.html", "backups_restore.html", "dashboard.html"]

# `class="app-row"` / `class="app-row {{...}}"` opening markup — [^-] excludes the
# derived class names (app-row-list, app-row-icon, ...).
ROW_OPEN_RE = re.compile(r'class="app-row[^-]')
ICON_RE = re.compile(r'app-row-icon')
ICON_ALLOW = {"app_row.html", "backups_apps.html"}  # backups_apps: the aligned expander header

failures = []

if not os.path.isdir(TPL):
    print("run from controller/ (internal/web/templates not found)")
    sys.exit(2)

files = {f: io.open(os.path.join(TPL, f), encoding="utf-8").read()
         for f in sorted(os.listdir(TPL)) if f.endswith(".html")}

if "app_row.html" not in files:
    failures.append("templates/app_row.html is missing — the canonical row partial")

for fname, src in files.items():
    n_open = len(ROW_OPEN_RE.findall(src))
    if fname == "app_row.html":
        if n_open != 1:
            failures.append("app_row.html: expected the row-opening markup exactly once, found %d" % n_open)
    elif n_open:
        failures.append("%s: hand-rolled app-row markup (%d occurrence(s)) — render through the app_list_row partial" % (fname, n_open))
    if ICON_RE.search(src) and fname not in ICON_ALLOW:
        failures.append("%s: app-row-icon outside the partial/allowlist — do not copy the icon markup" % fname)

for fname, pat, why in FORBIDDEN:
    src = files.get(fname, "")
    if re.search(pat, src):
        failures.append("%s: forbidden old structure %r survives (%s)" % (fname, pat, why))

for fname in MUST_USE:
    if '{{template "app_list_row"' not in files.get(fname, ""):
        failures.append("%s: does not render through the app_list_row partial" % fname)

if failures:
    print("app_row_dedup_gate: FAIL")
    for f in failures:
        print("  - " + f)
    sys.exit(1)
print("app_row_dedup_gate: OK (row markup single-sourced in app_row.html; %d templates scanned)" % len(files))
