#!/usr/bin/env python3
"""
gen_callgraph.py — svc/*.pgc 의 tpcall 관계를 추출해 아키텍처 복잡도 시각화용
데이터(app/ui/callgraph.js)를 만든다.

산출물:
  CG_MODULES  : 모듈 노드 11개 (id, 한글명, 서비스수, 교차호출 in/out)
  CG_MODEDGES : 모듈간 호출 엣지 (from,to,count)  ← 상위 얽힘 뷰
  CG_NODES    : 서비스 노드 552개 (id, module, x, y, deg)  ← 전체 hairball
  CG_EDGES    : 서비스 호출 엣지 494개 (from,to)
  CG_CHAINS   : 대표 심층 체인 (ACQUIRE 6단 등)  ← 하이라이트

레이아웃은 순수 파이썬 force-directed 로 여기서 미리 계산한다(외부 의존성 0).
결정적 재현을 위해 시드 고정.
"""
import glob, re, os, json, math, collections, random

random.seed(42)
SRC = os.path.join(os.path.dirname(__file__), "..", "app", "src")
OUT = os.path.join(os.path.dirname(__file__), "..", "app", "ui", "callgraph.js")

MODNM = {
    "ac": "매입", "au": "승인/한도", "st": "정산/수수료", "py": "지급",
    "lg": "원장", "cl": "마감", "rc": "대사", "vl": "정합성검증",
    "mm": "마스터", "mg": "전문게이트웨이", "cm": "공통",
}

# ── 1. 추출 ────────────────────────────────────────────────────────────────
node_mod, edges = {}, []
svc_count = collections.Counter()
for f in sorted(glob.glob(os.path.join(SRC, "*/svc/*.pgc"))):
    mod = os.path.normpath(f).split(os.sep)[-3]
    caller = os.path.basename(f)[:-4]
    node_mod[caller] = mod
    svc_count[mod] += 1
    txt = open(f, encoding="utf-8", errors="ignore").read()
    for callee in re.findall(r'tpcall\("([A-Z_0-9]+)"', txt):
        if callee != caller:
            edges.append((caller, callee))

# 호출에 관여하는 노드만 그래프에 (552개)
active = set(a for a, _ in edges) | set(b for _, b in edges)
deg = collections.Counter()
for a, b in edges:
    deg[a] += 1
    deg[b] += 1

# ── 2. 모듈 그래프 ──────────────────────────────────────────────────────────
modedge = collections.Counter()
for a, b in edges:
    ma, mb = node_mod.get(a, "cm"), node_mod.get(b, "cm")
    if ma != mb:
        modedge[(ma, mb)] += 1
mod_out = collections.Counter(); mod_in = collections.Counter()
for (a, b), c in modedge.items():
    mod_out[a] += c; mod_in[b] += c

# ── 3. 대표 심층 체인 (BFS 로 가장 깊은 경로 몇 개) ──────────────────────────
adj = collections.defaultdict(list)
for a, b in edges:
    if b not in adj[a]:
        adj[a].append(b)

def deepest_from(start, maxd=8):
    best = [start]
    stack = [(start, [start], {start})]
    while stack:
        n, path, seen = stack.pop()
        if len(path) > len(best):
            best = path
        if len(path) >= maxd:
            continue
        for m in adj.get(n, []):
            if m not in seen:
                stack.append((m, path + [m], seen | {m}))
    return best

chains = []
for root in ["ACQUIRE", "ACQ_SETTLELINK", "MG_RECV", "CL_DAILY", "PAY_APPROVE"]:
    if root in active:
        p = deepest_from(root)
        if len(p) >= 3:
            chains.append(p)

# ── 4. force-directed 레이아웃 (모듈별 초기 클러스터 → 스프링) ────────────────
nodes = sorted(active)
idx = {n: i for i, n in enumerate(nodes)}
N = len(nodes)
# 모듈을 원주에 배치해 같은 모듈끼리 뭉치게 초기화
mods = sorted(MODNM)
mang = {m: 2 * math.pi * i / len(mods) for i, m in enumerate(mods)}
X = [0.0] * N; Y = [0.0] * N
for n in nodes:
    m = node_mod.get(n, "cm")
    a = mang[m] + random.uniform(-0.35, 0.35)
    r = 340 + random.uniform(-60, 60)
    X[idx[n]] = math.cos(a) * r
    Y[idx[n]] = math.sin(a) * r

E = [(idx[a], idx[b]) for a, b in edges if a in idx and b in idx]
K = 78.0          # 이상 스프링 길이
REP = 3400.0      # 반발 상수
GRAV = 0.020      # 중심 중력 (박스 아티팩트 억제 — 원형 블롭 유지)
for it in range(340):
    fx = [0.0] * N; fy = [0.0] * N
    # 반발 (O(n^2), 552노드 = 15만/iter, 340회 ≈ 5천만 — 수초)
    for i in range(N):
        xi, yi = X[i], Y[i]
        for j in range(i + 1, N):
            dx = xi - X[j]; dy = yi - Y[j]
            d2 = dx * dx + dy * dy + 0.01
            f = REP / d2
            fdx = dx * f; fdy = dy * f
            fx[i] += fdx; fy[i] += fdy
            fx[j] -= fdx; fy[j] -= fdy
    # 인력 (엣지 스프링)
    for a, b in E:
        dx = X[a] - X[b]; dy = Y[a] - Y[b]
        d = math.sqrt(dx * dx + dy * dy) + 0.01
        f = (d - K) * 0.05
        fdx = dx / d * f; fdy = dy / d * f
        fx[a] -= fdx; fy[a] -= fdy
        fx[b] += fdx; fy[b] += fdy
    # 중심 중력 (반경 비례로 당겨 원형 유지)
    cool = 1.0 - it / 400.0
    for i in range(N):
        fx[i] -= X[i] * GRAV
        fy[i] -= Y[i] * GRAV
        X[i] += max(-22, min(22, fx[i])) * cool
        Y[i] += max(-22, min(22, fy[i])) * cool

# 정규화 → 종횡비 보존(단일 스케일), 중앙 정렬 → 0..1000 박스에 원형 블롭 유지
xs = [X[i] for i in range(N)]; ys = [Y[i] for i in range(N)]
minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
span = max(maxx - minx, maxy - miny) or 1
cx = (minx + maxx) / 2; cy = (miny + maxy) / 2
def nx(v): return round((v - cx) / span * 900 + 500, 1)
def ny(v): return round((v - cy) / span * 900 + 500, 1)

cg_nodes = [{"id": n, "m": node_mod.get(n, "cm"),
             "x": nx(X[idx[n]]), "y": ny(Y[idx[n]]),
             "d": deg[n]} for n in nodes]
cg_edges = [[a, b] for a, b in edges if a in idx and b in idx]
cg_modules = [{"id": m, "nm": MODNM[m], "svc": svc_count[m],
               "in": mod_in[m], "out": mod_out[m]} for m in mods]
cg_modedges = [{"f": a, "t": b, "c": c} for (a, b), c in modedge.items()]

data = {
    "MODULES": cg_modules,
    "MODEDGES": cg_modedges,
    "NODES": cg_nodes,
    "EDGES": cg_edges,
    "CHAINS": chains,
    "STAT": {"nodes": N, "edges": len(cg_edges), "callees": len(set(b for _, b in edges)),
             "modxedge": len(modedge), "maxdepth": max((len(c) for c in chains), default=0)},
}
payload = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
with open(OUT, "w", encoding="utf-8") as w:
    w.write("/* 자동생성: tools/gen_callgraph.py — svc tpcall 호출그래프 (레이아웃 사전계산). 손대지 말 것. */\n")
    w.write("window.CALLGRAPH = " + payload + ";\n")

# index.html 에 인라인 (게이트웨이는 index.html 만 서빙, 외부 .js 로드 불가).
# gen_screens.py 와 동일하게 마커 블록을 교체한다(재실행 idempotent).
import re as _re
IDX = os.path.join(os.path.dirname(__file__), "..", "app", "ui", "index.html")
h = open(IDX, encoding="utf-8").read()
block = ("<script>/* CG:START 자동생성 호출그래프 데이터 (tools/gen_callgraph.py) */\n"
         "window.CALLGRAPH=" + payload + ";\n/* CG:END */</script>")
if "/* CG:START" in h:
    h = _re.sub(r'<script>/\* CG:START.*?/\* CG:END \*/</script>', lambda m: block, h, count=1, flags=_re.S)
else:
    # 매니페스트 스크립트 바로 앞에 삽입 → 메인 앱보다 먼저 로드됨
    h = h.replace('<script>\n/* === 화면 매니페스트', block + '\n<script>\n/* === 화면 매니페스트', 1)
open(IDX, "w", encoding="utf-8").write(h)
print(f"callgraph.js: nodes={N} edges={len(cg_edges)} modxedge={len(modedge)} "
      f"maxdepth={data['STAT']['maxdepth']} chains={len(chains)}")
for c in chains:
    print("  체인:", " → ".join(c))
