# -*- coding: utf-8 -*-
"""D~J 카테고리 셀병합 정상화(off-by-one/누락/고아셀) + 선택적 행높이17.
값/스타일/K하이퍼링크/행수/B/L/M/N 보존. 자체검증 실패시 롤백.
사용: python _remerge.py <xlsx> [--apply] [--height] """
import openpyxl, os, shutil, sys, io, json
HDR=3
def load(path):
    return openpyxl.load_workbook(path)
def process(path, apply=False, height=False):
    rep={"file":path,"ok":False}
    wb=load(path); ws=wb.active; mx=ws.max_row
    if mx<HDR: 
        rep["skip"]="empty"; return rep
    # effective from existing merges (C..J)
    eff={}
    for r in range(HDR,mx+1):
        for c in range(3,11): eff[(r,c)]=ws.cell(r,c).value
    cat_merges=[]
    for mr in list(ws.merged_cells.ranges):
        if mr.min_row>=HDR and mr.min_col>=3 and mr.max_col<=10 and mr.min_col==mr.max_col:
            v=ws.cell(mr.min_row,mr.min_col).value
            for r in range(mr.min_row,mr.max_row+1): eff[(r,mr.min_col)]=v
            if mr.min_col>=4: cat_merges.append(str(mr))
        elif mr.min_row>=HDR and mr.min_col>=4 and mr.max_col<=10:
            # multi-col merge in cat area (unusual) -> fill & remove
            v=ws.cell(mr.min_row,mr.min_col).value
            for r in range(mr.min_row,mr.max_row+1):
                for c in range(mr.min_col,mr.max_col+1): eff[(r,c)]=v
            cat_merges.append(str(mr))
    def E(r,c):
        v=eff.get((r,c)); return str(v).strip() if v not in(None,"") else ""
    def path_of(r): return tuple(E(r,c) for c in range(4,11))
    orig_path={r:path_of(r) for r in range(HDR,mx+1)}
    orig_K=[(r,ws.cell(r,11).value) for r in range(HDR,mx+1)]
    orig_LMN=[(r,ws.cell(r,12).value,ws.cell(r,13).value,ws.cell(r,14).value) for r in range(HDR,mx+1)]
    orig_B=[(r,ws.cell(r,2).value) for r in range(HDR,mx+1)]
    # orphan fill: blank cat cell with deeper content -> inherit from row above
    orphans=[]
    for r in range(HDR+1,mx+1):
        for c in range(4,11):
            if E(r,c)=="" and any(E(r,cc)!="" for cc in range(c+1,11)) and E(r-1,c)!="":
                eff[(r,c)]=eff[(r-1,c)]; orphans.append((r,c))
    def parent(r,c): return tuple(E(r,cc) for cc in range(3,c))  # C..c-1
    target=[]
    for c in range(4,11):
        r=HDR
        while r<=mx:
            v=E(r,c)
            if v=="": r+=1; continue
            r2=r
            while r2+1<=mx and E(r2+1,c)==v and parent(r2+1,c)==parent(r,c): r2+=1
            if r2>r: target.append((c,r,r2))
            r=r2+1
    existing=set()
    for s in cat_merges:
        # parse "X1:Y2"
        from openpyxl.utils.cell import range_boundaries
        c1,r1,c2,r2=range_boundaries(s)
        if c1==c2: existing.add((c1,r1,r2))
    tgt=set(target)
    rep["orphans"]=[(r,openpyxl.utils.get_column_letter(c)) for (r,c) in orphans]
    rep["add"]=sorted([(openpyxl.utils.get_column_letter(c),r1,r2) for (c,r1,r2) in (tgt-existing)])
    rep["removed"]=sorted([(openpyxl.utils.get_column_letter(c),r1,r2) for (c,r1,r2) in (existing-tgt)])
    rep["n_target"]=len(tgt); rep["n_existing"]=len(existing); rep["n_change"]=len(tgt-existing)
    if not apply:
        rep["ok"]=True; rep["dry"]=True; return rep
    if not (tgt-existing) and not orphans and not height:
        rep["ok"]=True; rep["nochange"]=True; return rep
    # ---- APPLY ----
    bdir=os.path.join(os.path.dirname(path),"_backup")
    os.makedirs(bdir,exist_ok=True)
    bpath=os.path.join(bdir, os.path.splitext(os.path.basename(path))[0]+"_backup_셀병합전.xlsx")
    shutil.copy2(path,bpath); rep["backup"]=bpath
    for s in cat_merges:
        try: ws.unmerge_cells(s)
        except Exception: pass
    # write eff values to all cat cells
    for r in range(HDR,mx+1):
        for c in range(4,11):
            v=eff.get((r,c))
            ws.cell(r,c).value=(v if v not in(None,"") else None)
    # blank non-top cells of runs
    for (c,r1,r2) in target:
        for r in range(r1+1,r2+1): ws.cell(r,c).value=None
    # re-merge
    for (c,r1,r2) in target:
        ws.merge_cells(start_row=r1,start_column=c,end_row=r2,end_column=c)
    if height:
        ws.sheet_format.defaultRowHeight=17; ws.sheet_format.customHeight=True
        for r in range(1,mx+1): ws.row_dimensions[r].height=17
    wb.save(path)
    # ---- VERIFY (reload) ----
    wb2=load(path); ws2=wb2.active
    e2={}
    for r in range(HDR,ws2.max_row+1):
        for c in range(3,11): e2[(r,c)]=ws2.cell(r,c).value
    for mr in ws2.merged_cells.ranges:
        if mr.min_row>=HDR and mr.min_col>=3 and mr.max_col<=10:
            v=ws2.cell(mr.min_row,mr.min_col).value
            for r in range(mr.min_row,mr.max_row+1):
                for c in range(mr.min_col,mr.max_col+1): e2[(r,c)]=v
    def E2(r,c):
        v=e2.get((r,c)); return str(v).strip() if v not in(None,"") else ""
    fails=[]
    if ws2.max_row!=mx: fails.append("rowcount %d!=%d"%(ws2.max_row,mx))
    orphan_set=set(orphans)
    for r in range(HDR,mx+1):
        npath=tuple(E2(r,c) for c in range(4,11))
        if npath!=orig_path[r]:
            # allowed only if diff positions are orphan-filled
            diffcols=[c for i,c in enumerate(range(4,11)) if npath[i]!=orig_path[r][i]]
            if not all((r,c) in orphan_set for c in diffcols):
                fails.append("path r%d %s->%s"%(r,orig_path[r],npath))
    for (r,k) in orig_K:
        if ws2.cell(r,11).value!=k: fails.append("K r%d"%r)
    for (r,b) in orig_B:
        if ws2.cell(r,2).value!=b: fails.append("B r%d"%r)
    for (r,l,m,n) in orig_LMN:
        if (ws2.cell(r,12).value,ws2.cell(r,13).value,ws2.cell(r,14).value)!=(l,m,n): fails.append("LMN r%d"%r)
    # merge overlap / boundary check
    ranges=[(mr.min_col,mr.min_row,mr.max_row) for mr in ws2.merged_cells.ranges if mr.min_col==mr.max_col and 4<=mr.min_col<=10]
    seen={}
    for (c,r1,r2) in ranges:
        for r in range(r1,r2+1):
            if (c,r) in seen: fails.append("overlap c%d r%d"%(c,r))
            seen[(c,r)]=1
    rep["fails"]=fails[:20]
    if fails:
        shutil.copy2(bpath,path)  # rollback
        rep["rolledback"]=True; rep["ok"]=False
    else:
        rep["ok"]=True; rep["n_merges_after"]=len(ranges)
    return rep

if __name__=="__main__":
    args=sys.argv[1:]
    path=args[0]
    apply="--apply" in args; height="--height" in args
    r=process(path,apply=apply,height=height)
    print(json.dumps(r,ensure_ascii=False,indent=1))
