#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
obj_edit.py — операции над ExternalDataProcessor.json (объектные метаданные .epf
в формате v8unpack: вложенные массивы строк). Дополняет form_edit.py/form_inspect.py,
которые правят ФОРМЫ; этот тул правит ОБЪЕКТ (табличные части, типы реквизитов, имя/синоним).

Формат, подтверждённый реверсом (см. reference/object_metadata.md):
  Узел реквизита ТЧ:
    ["27", ["2",
              ["3", ["1","0","<BINDING-UUID>"], "\"Имя\"", ["1","\"ru\"","\"Синоним\""], "\"\"","0","0","00000000-...","0"],
              ["\"Pattern\"", <ТИП>]
           ], <хвост-флаги...>]
  ТИП:  ref=["\"#\"","<TypeId>"] | string=["\"S\""] | number=["\"N\"","15","2","0"] | bool=["\"B\""] | date=["\"D\"",...]
  Колонка формы ссылается на реквизит по BINDING-UUID (см. form_inspect cols).
  Имя/Синоним обработки живут в бинарном `header` (поля name/name2 в JSON — ПРОИЗВОДНЫЕ,
  пакер их игнорит; менять надо в header). Имя встречается в файле НЕСКОЛЬКО раз.

Команды:
  attrs   <obj.json>                       — перечислить ВСЕ реквизиты ('27'-узлы): имя, binding-uuid, тип
  typeid  <config_dir_or_xml> <Kind.Имя>   — TypeId (category Ref) из XML-выгрузки конфигурации
                                             напр.: typeid НоваяКонфа Catalog.Организации
  retype  <obj.json> --uuid U --name N --syn S --type (ref:<TypeId>|bool|string|number|date)
                                           [--donor F]  — перетипизировать реквизит ПО МЕСТУ
                                             (сохраняет binding-uuid → колонка формы следует автоматически)
  rename  <obj.json> --name N [--syn S]    — сменить ИМЯ обработки во ВСЁМ файле (+header) и синоним
"""
import sys, json, os, re, glob, copy, argparse

UUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')

def load(p): return json.load(open(p, encoding='utf-8'))
def save(p, d): json.dump(d, open(p,'w',encoding='utf-8'), ensure_ascii=False, indent=2)

def iter_attr_nodes(o):
    """yield каждый '27'-узел реквизита."""
    if isinstance(o, dict):
        for v in o.values(): yield from iter_attr_nodes(v)
    elif isinstance(o, list):
        if o and o[0]=='27' and len(o)>=2 and isinstance(o[1],list) and o[1][0]=='2' and len(o[1])>2:
            yield o
        for v in o: yield from iter_attr_nodes(v)

def attr_info(node):
    desc = node[1][1]                 # ["3", ["1","0",uuid], "\"Имя\"", ["1","ru","Син"], ...]
    uuid = desc[1][2]
    name = desc[2].strip('"')
    patt = node[1][2]                 # ["\"Pattern\"", <тип>]
    return name, uuid, patt

def patt_kind(patt):
    try:
        t = patt[1]
        code = t[0].strip('"')
        if code == '#': return 'ref:'+t[1]
        return {'S':'string','N':'number','B':'bool','D':'date'}.get(code, code)
    except Exception:
        return '?'

def cmd_attrs(args):
    d = load(args.obj)
    seen=set()
    for n in iter_attr_nodes(d):
        name,uuid,patt = attr_info(n)
        print(f"  {name:34s} {uuid}  {patt_kind(patt)}")

def cmd_typeid(args):
    src = args.config
    kind, name = args.kindname.split('.', 1)
    folder = {'Catalog':'Catalogs','Document':'Documents','ChartOfAccounts':'ChartsOfAccounts',
              'Enum':'Enums','InformationRegister':'InformationRegisters',
              'AccumulationRegister':'AccumulationRegisters'}.get(kind, kind+'s')
    cands=[]
    if os.path.isfile(src): cands=[src]
    else:
        cands = glob.glob(os.path.join(src,'**',folder,name+'.xml'), recursive=True) \
             or glob.glob(os.path.join(src,'**',name+'.xml'), recursive=True)
    if not cands: sys.exit(f"XML метаданных {kind}.{name} не найден под {src}")
    txt=open(cands[0],encoding='utf-8',errors='replace').read()
    m=re.search(r'category="Ref">\s*<[^>]*TypeId>([0-9a-fA-F-]+)<', txt)
    if not m: sys.exit("TypeId (category Ref) не найден в "+cands[0])
    print(m.group(1))

def find_template(d, want_kind, donor):
    """Найти '27'-узел нужного рода (ref/bool/string/number/date) как образец."""
    def search(doc):
        for n in iter_attr_nodes(doc):
            k = patt_kind(attr_info(n)[2])
            if want_kind=='ref' and k.startswith('ref:'): return n
            if want_kind==k: return n
        return None
    n = search(d)
    if n: return n
    if donor:
        return search(load(donor))
    # авто-поиск среди соседних обработок ОбработкиЭкспорт
    base = os.path.expanduser('~/Downloads/ОбработкиЭкспорт')
    for f in glob.glob(os.path.join(base,'*','ExternalDataProcessor.json')):
        try: n = search(load(f))
        except Exception: continue
        if n: return n
    return None

def cmd_retype(args):
    d = load(args.obj)
    typ = args.type
    want_kind = 'ref' if typ.startswith('ref:') else typ
    type_uuid = typ.split(':',1)[1] if typ.startswith('ref:') else None
    tpl = find_template(d, want_kind, args.donor)
    if tpl is None:
        sys.exit(f"Не нашёл образец узла рода '{want_kind}'. Укажи --donor <ExternalDataProcessor.json> с таким типом.")
    def build(binding_uuid):
        n = copy.deepcopy(tpl)
        desc = n[1][1]
        desc[1][2] = binding_uuid
        desc[2] = '"%s"' % args.name
        desc[3] = ['1','"ru"','"%s"'%(args.syn if args.syn else args.name)]
        if type_uuid:
            n[1][2] = ['"Pattern"', ['"#"', type_uuid]]
        return n
    cnt=[0]
    def rep(o):
        if isinstance(o,dict): return {k:rep(v) for k,v in o.items()}
        if isinstance(o,list):
            if o and o[0]=='27' and len(o)>=2 and isinstance(o[1],list) and o[1][0]=='2' and len(o[1])>2:
                if o[1][1][1][2]==args.uuid:
                    cnt[0]+=1; return build(args.uuid)
            return [rep(v) for v in o]
        return o
    d=rep(d)
    if cnt[0]==0: sys.exit("Реквизит с binding-uuid %s не найден"%args.uuid)
    save(args.obj, d)
    print(f"retype OK: {args.uuid} -> '{args.name}' [{typ}]  (заменено узлов: {cnt[0]})")

def cmd_rename(args):
    d = load(args.obj)
    old = d.get('name')
    if not old: sys.exit("в файле нет поля name")
    OLD='"%s"'%old; NEW='"%s"'%args.name
    cnt=[0]
    def rep(o):
        if isinstance(o,dict): return {k:rep(v) for k,v in o.items()}
        if isinstance(o,list): return [rep(v) for v in o]
        if isinstance(o,str) and o==OLD: cnt[0]+=1; return NEW
        return o
    d=rep(d)                 # заменяет имя в header И во всех копиях внутри структуры
    d['name']=args.name
    if args.syn: d['name2']={'ru':args.syn}
    save(args.obj, d)
    print(f"rename OK: '{old}' -> '{args.name}'  (токенов заменено: {cnt[0]}); syn={d.get('name2')}")
    print("ПРОВЕРЬ: имя/синоним переживают pack→unpack (epf.sh roundtrip; имя/синоним в header).")

TCH_MARK = "5d24a9d1-098e-11d6-b9b8-0050bae0a95d"   # маркер коллекции реквизитов ТЧ в header

def _find_block(node, tag):
    if isinstance(node, list):
        if node and node[0] == tag: return node
        for el in node:
            r = _find_block(el, tag)
            if r is not None: return r
    return None

def cmd_add_attr(args):
    """Клонировать реквизит ТЧ (донор по uuid) в ту же ТЧ с новым uuid/именем/синонимом.
    Патчит 3 места: контейнер реквизитов в header + copyinfo['33'] + copyinfo['22'].
    Тип берётся от донора (для другого типа потом retype по новому uuid)."""
    import copy, uuid as _uuid
    d = load(args.obj); D = args.donor_uuid
    new = args.new_uuid or str(_uuid.uuid4())
    assert new not in json.dumps(d, ensure_ascii=False), "new-uuid уже есть в файле"
    # имя/синоним донора
    donor_name = donor_syn = None
    for n in iter_attr_nodes(d):
        nm, u, _ = attr_info(n)
        if u == D:
            desc = n[1][1]; donor_name = desc[2]; donor_syn = desc[3][2]; break
    assert donor_name, f"донор-реквизит {D} не найден"
    NEWNAME = '"'+args.name+'"'; NEWSYN = '"'+args.syn+'"'
    REPL = {D:new, donor_name:NEWNAME, donor_syn:NEWSYN}
    def repl(x):
        if isinstance(x, list): return [repl(e) for e in x]
        return REPL.get(x, x)
    # 1) header: контейнер с TCH_MARK, среди элементов которого есть донор
    def find_cont(node):
        if isinstance(node, list):
            if node and node[0]==TCH_MARK and D in json.dumps(node, ensure_ascii=False): return node
            for el in node:
                r = find_cont(el)
                if r is not None: return r
        return None
    cont = find_cont(d["header"]); assert cont, "контейнер ТЧ с донором не найден"
    ai = next(i for i in range(2,len(cont)) if D in json.dumps(cont[i], ensure_ascii=False))
    cont.append(repl(copy.deepcopy(cont[ai]))); cont[1] = str(len(cont)-2)
    # 2) copyinfo '33' (uuid→ТЧ/реквизит) и '22' (uuid→категория)
    ci = d.get("copyinfo")
    b33 = _find_block(ci,"33"); b22 = _find_block(ci,"22")
    e33 = next(e for e in b33[1:] if isinstance(e,list) and e and e[0]==D)
    b33.append(repl(copy.deepcopy(e33)))
    e22 = next(e for e in b22[1:] if isinstance(e,list) and isinstance(e[0],list) and e[0][1]==D)
    b22.append(repl(copy.deepcopy(e22)))
    save(args.obj, d)
    print(f"✅ добавлен реквизит {args.name!r} uuid={new} (клон {D}); ТЧ-реквизитов теперь {cont[1]}")
    print("   ⚠ дальше: поле формы (клон '37'-колонки + узел дерева, привязка → новый uuid) и заполнение в модуле.")

def main():
    ap=argparse.ArgumentParser(description='Правка объектных метаданных .epf (v8unpack)')
    sub=ap.add_subparsers(dest='cmd', required=True)
    p=sub.add_parser('attrs'); p.add_argument('obj'); p.set_defaults(fn=cmd_attrs)
    p=sub.add_parser('typeid'); p.add_argument('config'); p.add_argument('kindname'); p.set_defaults(fn=cmd_typeid)
    p=sub.add_parser('retype'); p.add_argument('obj'); p.add_argument('--uuid',required=True)
    p.add_argument('--name',required=True); p.add_argument('--syn'); p.add_argument('--type',required=True)
    p.add_argument('--donor'); p.set_defaults(fn=cmd_retype)
    p=sub.add_parser('rename'); p.add_argument('obj'); p.add_argument('--name',required=True)
    p.add_argument('--syn'); p.set_defaults(fn=cmd_rename)
    p=sub.add_parser('add-attr'); p.add_argument('obj'); p.add_argument('--donor-uuid',required=True,dest='donor_uuid')
    p.add_argument('--name',required=True); p.add_argument('--syn',required=True)
    p.add_argument('--new-uuid',dest='new_uuid'); p.set_defaults(fn=cmd_add_attr)
    a=ap.parse_args(); a.fn(a)

if __name__=='__main__':
    main()
