#!/usr/bin/env python3
"""form_inspect.py — разбор Form.elem.json внешней обработки 1С (формат v8unpack).

Секции elem.json:
  props    — реквизиты формы (атрибуты). [{name,id,raw}]; raw[0]=="9", raw[1]==[id]
  tree     — иерархия элементов (name/type/child) БЕЗ raw
  data     — словарь {путь: {raw, ver}}; raw элемента (таблица + её raw содержит
             командные панели и КНОПКИ как вложенные массивы)
  commands — локальные команды формы (или null)

Использование:
  form_inspect.py F.json                         # обзор props/tree/data
  form_inspect.py F.json --buttons               # кнопки: имя, контейнер, привязка команды
  form_inspect.py F.json --readonly TABLE [--siblings S1.json S2.json ...]
                                                 # триангуляция флага «Только просмотр» таблицы
"""
import json, sys, argparse, re

GUID = re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')

def load(p): return json.load(open(p, encoding="utf-8"))

def walk_tree(n, dep=0):
    print("  "*dep + f"{n.get('type')} {n.get('name')!r}")
    for c in n.get("child", []) or []:
        walk_tree(c, dep+1)

def overview(d):
    print("TOP keys:", list(d.keys()))
    print("\n--- props (реквизиты формы) ---")
    for p in d.get("props") or []:
        print(f"   {p['name']}  id={p['id']}")
    print("\n--- tree (иерархия элементов) ---")
    for n in d.get("tree") or []:
        walk_tree(n)
    print("\n--- data keys (элементы с raw) ---")
    for k in (d.get("data") or {}):
        print("  ", k)
    print("\ncommands:", "есть" if d.get("commands") else "нет (null)")

def find_buttons(raw, path=()):
    """вернуть [(path, button_array)] — под-массивы с head '31' (кнопка)."""
    out = []
    if isinstance(raw, list):
        if raw and raw[0] == "31":
            out.append((path, raw))
        for i, el in enumerate(raw):
            out += find_buttons(el, path + (i,))
    return out

def container_of(raw, path):
    """ближайший контейнер ('22' командная панель / '55' таблица) над кнопкой."""
    node, chain = raw, []
    for i in path:
        chain.append(node); node = node[i]
    for anc in reversed(chain):
        if isinstance(anc, list) and anc and anc[0] in ("22", "55"):
            nm = [x for x in anc[:8] if isinstance(x, str) and x.startswith('"')]
            return anc[0], (nm[0].strip('"') if nm else "?")
    return "?", "?"

def button_binding(btn):
    """из raw кнопки вытащить (имя, uuid_команды, привязка_параметра)."""
    name = next((x.strip('"') for x in btn if isinstance(x, str) and x.startswith('"')), "?")
    uuid = None; param = None
    for i, el in enumerate(btn):
        if isinstance(el, list) and len(el) == 2 and el[0] in ("0", "1") and isinstance(el[1], str) and GUID.fullmatch(el[1] or ""):
            uuid = el[1]
            # следом часто идёт привязка параметра ["1",["<id>"]]
            if i+1 < len(btn) and isinstance(btn[i+1], list) and len(btn[i+1]) == 2 and isinstance(btn[i+1][1], list):
                param = btn[i+1][1]
            break
    return name, uuid, param

def buttons_report(d):
    data = d.get("data") or {}
    found = False
    for k, v in data.items():
        raw = v.get("raw")
        if not isinstance(raw, list):
            continue
        for path, btn in find_buttons(raw):
            ctype, cname = container_of(raw, path)
            name, uuid, param = button_binding(btn)
            if name == "?" and uuid is None:
                continue  # артефакт (поле с представлением-кнопкой), не командная кнопка
            print(f"кнопка {name!r}")
            print(f"   data-ключ : {k}")
            print(f"   контейнер : тип={ctype} имя={cname!r}  ('22'=командная панель)")
            print(f"   команда   : uuid={uuid}  (общая команда, если uuid из НоваяКонфа/CommonCommands)")
            print(f"   привязка параметра команды → реквизит id={param}")
            print(f"      ⚠ если такого id НЕТ в props — 1С СПРЯЧЕТ кнопку. Добавь реквизит с этим id.")
            found = True
    if not found:
        print("кнопок (тип '31') не найдено")

def table_raw(d, table):
    v = (d.get("data") or {}).get(table)
    if not v: return None
    raw = v.get("raw")
    return raw if isinstance(raw, list) and raw and raw[0] == "55" else None

def readonly_triangulate(d, table, siblings):
    raw = table_raw(d, table)
    if raw is None:
        print(f"таблица {table!r} (тип '55') не найдена"); return
    # собрать ведущие скаляры
    def scalars(r): return {i: r[i] for i in range(min(60, len(r))) if isinstance(r[i], str)}
    me = scalars(raw)
    sibs = []
    for sp in siblings:
        sd = load(sp)
        sr = table_raw(sd, table)   # СТРОГО одноимённая таблица — без fallback (иначе шум)
        if sr:
            sibs.append((sp.split("/")[-3] if "/" in sp else sp, scalars(sr)))
        else:
            print(f"   (пропуск {sp}: нет таблицы {table!r} — триангуляция требует ОДНОИМЁННУЮ таблицу)")
    if not sibs:
        print("нет ни одного родственника с такой таблицей — добавь формы, где есть та же таблица"); return
    print(f"=== флаги ведущих скаляров таблицы {table!r}: НАША vs родственники ===")
    print(f"{'idx':>4}  НАША  " + "  ".join(f"{n[:10]:>10}" for n,_ in sibs))
    strong, weak = [], []
    for i in range(60):
        mine = me.get(i)
        if mine is None: continue
        row = [s.get(i) for _, s in sibs]
        differ = all(x is not None and x != mine for x in row)
        # сильный кандидат: наша='1', у ВСЕХ родственников='0' (классический «Только просмотр»)
        is_strong = mine == "1" and all(x == "0" for x in row)
        line = f"{i:>4}  {mine:>4}  " + "  ".join(f"{(x if x is not None else '·'):>10}" for x in row)
        if is_strong:
            line += "  <<<<< СИЛЬНЫЙ кандидат «Только просмотр» (наша=1, все=0)"; strong.append(i)
        elif differ:
            line += "  <<< отличается (вряд ли readonly)"; weak.append(i)
        print(line)
    print(f"\nСИЛЬНЫЕ кандидаты «Только просмотр» (наша=1, все родственники=0): {strong}")
    print(f"прочие отличия (НЕ трогать вслепую): {weak}")
    print("⚠ Триангуляция по РАЗНЫМ формам даёт несколько кандидатов — это сужение, не ответ.")
    print("  Надёжнее всего: diff нашей таблицы против ЗАВЕДОМО редактируемой копии ТОЙ ЖЕ таблицы")
    print("  (тогда отличие ровно одно). Иначе — выбери кандидат и ПОДТВЕРДИ в 1С.")
    if 36 in strong:
        print("✅ Для семейства «список счетов» подтверждён index=36 (ТолькоПросмотр).")
        print(f"  → form_edit.py set-table-readonly --form <F> --table {table} --value 0   (--index 36 по умолчанию)")
    elif strong:
        print(f"  → проверь в 1С кандидат(ы) {strong}; снять: set-table-readonly --index <I> --value 0")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("form")
    ap.add_argument("--buttons", action="store_true")
    ap.add_argument("--readonly", metavar="TABLE")
    ap.add_argument("--siblings", nargs="*", default=[])
    a = ap.parse_args()
    d = load(a.form)
    if a.buttons: buttons_report(d)
    elif a.readonly: readonly_triangulate(d, a.readonly, a.siblings)
    else: overview(d)

if __name__ == "__main__":
    main()
