# -*- coding: utf-8 -*-
"""규칙 B 재귀판: 탭 레벨(G→H→I…)에서 '자식이 전부 페이지'면 1행으로 합치고 M=합.
메뉴 카테고리(D·E·F)는 보존 — 합치기는 lc>=7(G 이하 탭 레벨)에서만 수행.

각 레벨 lc 에서:
  부모(D..lc-1) 동일 + lc 비어있지 않음 으로 형제 묶음 → 그 묶음이
    · 전부 L=페이지, · lc보다 깊은 카테고리열 모두 비어있음(잎)
  이면 1행으로 합침: lc 이하 카테고리열 비움, M=합, K=첫 행 URL,
  공공누리 O=유형 합집합(오름차순,콤마,공백없음)/P=게시판우선/Q=하나라도 Y.
깊은→얕은 순으로 반복(안정될 때까지) → G→H 합친 뒤 F→G까지 자연 연쇄.

평탄화/재병합(D/E/F)/순번/하이퍼링크는 _dedup_all.write_back 재사용.

사용:
  python -X utf8 _collapse_recursive.py dry  <엑셀경로 | 기관명...>
  python -X utf8 _collapse_recursive.py run  <엑셀경로 | 기관명...>
  (기관명 없이 경로 1개만 줘도 됨)
"""
import os, re, sys, shutil, importlib.util, warnings
import openpyxl
warnings.filterwarnings('ignore')

spec = importlib.util.spec_from_file_location('dd', __file__.replace('_collapse_recursive.py', '_dedup_all.py'))
dd = importlib.util.module_from_spec(spec); spec.loader.exec_module(dd)

CAT = [4, 5, 6, 7, 8, 9, 10]          # D E F G H I J
COLLAPSE_LEVELS = [10, 9, 8, 7]       # 탭 레벨만(깊은→얕은). E(6)·D(5)는 제외=카테고리 보존
TYPE_PAT = re.compile(r'(\d)\s*유형')
EXCLUDE = {'계룡시'}                   # 검수완료


def mval(x):
    try:
        return int(x)
    except Exception:
        return 1


def merge_kogl(grp):
    types = set()
    for g in grp:
        o = g['vals'].get(15)
        if isinstance(o, str):
            for m in TYPE_PAT.finditer(o):
                types.add(int(m.group(1)))
    if types:
        O = ','.join(f'{n}유형' for n in sorted(types))
    else:
        O = '미부착'
    Ps = [g['vals'].get(16) for g in grp]
    P = '게시판' if '게시판' in Ps else ('게시물' if '게시물' in Ps else None)
    Qs = [g['vals'].get(17) for g in grp]
    Q = 'Y' if 'Y' in Qs else ('N' if 'N' in Qs else None)
    return O, (P if O != '미부착' else None), (Q if O != '미부착' else None)


def collapse_level(rows, lc):
    parent = [c for c in CAT if c < lc]
    deeper = [c for c in CAT if c > lc]
    out, groups = [], []
    i, n = 0, len(rows)
    while i < n:
        v = rows[i]['vals']
        if v.get(lc) not in (None, ''):
            j = i
            while (j < n and all(rows[j]['vals'].get(c) == v.get(c) for c in parent)
                   and rows[j]['vals'].get(lc) not in (None, '')):
                j += 1
            grp = rows[i:j]
            all_page = all(g['vals'].get(12) == '페이지' for g in grp)
            leaf = all(all(g['vals'].get(c) in (None, '') for c in deeper) for g in grp)
            has_attach = any(g['vals'].get(15) not in (None, '', '미부착') for g in grp)
            if len(grp) >= 2 and all_page and leaf:
                msum = sum(mval(g['vals'].get(13)) for g in grp)
                O, P, Q = merge_kogl(grp)
                first = dict(grp[0]); nv = dict(first['vals'])
                for c in [lc] + deeper:
                    nv[c] = None
                nv[13] = msum; nv[15] = O; nv[16] = P; nv[17] = Q
                first['vals'] = nv
                out.append(first)
                groups.append({'lc': lc, 'path': [v.get(c) for c in parent],
                               'label': v.get(lc), 'n': len(grp),
                               'ms': [mval(g['vals'].get(13)) for g in grp],
                               'sum': msum, 'O': O, 'attach': has_attach,
                               'names': [g['vals'].get(lc) for g in grp]})
                i = j; continue
            out.extend(grp); i = j; continue
        out.append(rows[i]); i += 1
    return out, groups


def collapse_all_levels(rows):
    all_groups = []
    changed = True
    while changed:
        changed = False
        for lc in COLLAPSE_LEVELS:
            rows, groups = collapse_level(rows, lc)
            if groups:
                changed = True
                all_groups.extend(groups)
    return rows, all_groups


def resolve_paths(args):
    """경로 또는 기관명 목록 → [(name, xlsx경로)]"""
    paths = []
    for a in args:
        if a.lower().endswith('.xlsx') or os.path.sep in a:
            paths.append((os.path.splitext(os.path.basename(a))[0], a))
    names = [a for a in args if not (a.lower().endswith('.xlsx') or os.path.sep in a)]
    for prov, idx, name in dd.SITES:
        if names and name not in names:
            continue
        if not names:
            continue
        paths.append((name, dd.xpath(prov, idx, name)))
    return paths


LV = {7: 'F→G', 8: 'G→H', 9: 'H→I', 10: 'I→J'}


def main():
    mode = sys.argv[1] if len(sys.argv) > 1 else 'dry'
    rest = [a for a in sys.argv[2:] if not a.startswith('--')]
    targets = resolve_paths(rest)
    if not targets:
        print('대상 없음. 경로 또는 기관명을 지정하세요.'); return
    for name, xp in targets:
        if name in EXCLUDE:
            print(f'{name}: 검수완료 제외'); continue
        if not os.path.exists(xp):
            print(f'{name}: 엑셀 없음 ({xp})'); continue
        wb = openpyxl.load_workbook(xp); ws = wb.active
        rows = dd.load_flat(ws)
        out, groups = collapse_all_levels(rows)
        removed = len(rows) - len(out)
        bylv = {}
        for g in groups:
            bylv.setdefault(g['lc'], 0)
            bylv[g['lc']] += 1
        lvstr = ' '.join(f"{LV.get(k,k)}:{v}" for k, v in sorted(bylv.items()))
        print(f'\n=== {name} === {len(rows)}→{len(out)} (제거 {removed})  [{lvstr}]')
        for g in groups:
            ms = '+'.join(str(m) for m in g['ms'])
            star = ' ★부착' if g['attach'] else ''
            print(f"  [{LV.get(g['lc'],g['lc'])}] {' > '.join(str(x) for x in g['path'] if x)} > {g['label']}"
                  f"  ({g['n']}개) → M={ms}={g['sum']} O={g['O']}{star}")
        if mode == 'run' and groups:
            bak = xp.replace('.xlsx', '_backup_재귀합치기전.xlsx')
            shutil.copy(xp, bak)
            dd.write_back(ws, out)
            try:
                wb.save(xp)
            except PermissionError:
                wb.save(xp.replace('.xlsx', '_LP.xlsx')); print(f'   !! {name} 잠김→_LP')
            else:
                print(f'   저장 완료. 백업: {os.path.basename(bak)}')
        elif mode != 'run':
            print('   (DRY)')


if __name__ == '__main__':
    main()
