#!/usr/bin/env python3
"""카드 안에서 **같은 지시가 몇 번 되풀이되나**를 잰다 (무료).

## 왜 재나

발견 ④ — `render_strategy.constraints` 가 바로 위 `spatial_consistency` 를
요약해 되풀이한다. 실측하면 `body-part close-up` 한 지시가 **다섯 번**
나온다(constraints 2 + spatial 3).

★**걷기 전에 재는 이유**: 08-24 `a675c352` 가 「설명 걷기」를 기각했다.
 같은 말이 여러 번 나오는 것이 **강조로 작동할** 수도 있다. 그러니 먼저
 「무엇이 몇 번, 어떤 모양으로」를 밝히고, 걷을지는 그 다음에 정한다.
★이 도구는 **좋고 나쁨을 말하지 않는다.** 세고 어디인지 짚을 뿐이다.

## 무엇을 세나

카드는 값(무엇을 그리나)과 규칙(어떻게 그리나)을 같이 담는다. 규칙 쪽만
본다 — `constraints` 계열과 `*_rule` 계열.

세는 단위는 **지시 낱말 묶음**이 아니라 **실제로 나온 문구**다. 낱말을
내가 골라 세면 내가 고른 것만 나온다.

## 쓰는 법

    python tools/prompt_measure/card_redundancy.py            # 기본 카드
    python tools/prompt_measure/card_redundancy.py --cp PATH  # 체크포인트 실물
"""
from __future__ import annotations

import argparse
import json
import re
import sys
from collections import defaultdict
from typing import Any, Dict, List, Tuple

sys.path.insert(0, "/Users/manta/Documents/Projects/TheRoad-I1/scratchpad")
import _opik_env  # noqa: E402,F401  ★cwd 를 backend 로 고정

_문장 = re.compile(r"[.;]\s+|\n")
_불용 = frozenset("""a an the of to in on at for and or is are be it its this that
with as by from into not no do does can may must should when if than then them
they their there here which what who where how each any all both either every
same other another such only just also more most less least very much many few
one two three same""".split())


def _낱말(s: str) -> frozenset:
    return frozenset(w for w in re.findall(r"[a-z][a-z\-]{2,}", s.lower())
                     if w not in _불용)


def _조각들(x: Any, 경로: str = "") -> List[Tuple[str, str]]:
    """(경로, 문구) 목록. 규칙 계열 문자열만 걷는다."""
    out: List[Tuple[str, str]] = []
    if isinstance(x, str):
        for 절 in _문장.split(x):
            절 = 절.strip()
            if len(절) >= 25:
                out.append((경로, 절))
    elif isinstance(x, dict):
        for k, v in x.items():
            out.extend(_조각들(v, f"{경로}.{k}" if 경로 else k))
    elif isinstance(x, list):
        for i, v in enumerate(x):
            out.extend(_조각들(v, f"{경로}[{i}]"))
    return out


def _겹침(a: frozenset, b: frozenset) -> float:
    if not a or not b:
        return 0.0
    return len(a & b) / min(len(a), len(b))


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--cp", help="체크포인트 manifest.json (없으면 기본 카드)")
    ap.add_argument("--min", type=float, default=0.6,
                    help="같은 지시로 볼 낱말 겹침 (기본 0.6)")
    a = ap.parse_args()

    if a.cp:
        d = json.loads(open(a.cp, encoding="utf-8").read())
        장면 = (d.get("data") or {}).get("scenes") or []
        카드들 = [(f"S{s.get('scene_index')}sh{s.get('_shot_index')}",
                   (s.get("render_prompt_card") or {}).get("render_strategy") or {})
                  for s in 장면 if isinstance(s, dict)]
        카드들 = [(n, c) for n, c in 카드들 if c]
    else:
        from app.core.steps.render_prompt_card import (
            _build_render_strategy_constraints, _build_spatial_consistency_dict,
        )
        카드들 = [("기본", {
            "spatial_consistency": _build_spatial_consistency_dict(),
            "constraints": _build_render_strategy_constraints(),
        })]

    이름, 카드 = 카드들[0]
    조각 = _조각들(카드)
    print(f"판 {len(카드들)}개 · 표본 {이름} · 규칙 문구 {len(조각)}개 · "
          f"{sum(len(t) for _, t in 조각):,}자\n")

    # 낱말 묶음이 겹치는 문구끼리 묶는다
    묶음: List[List[Tuple[str, str, frozenset]]] = []
    for 경로, 문구 in 조각:
        w = _낱말(문구)
        for g in 묶음:
            if any(_겹침(w, ww) >= a.min for _, _, ww in g):
                g.append((경로, 문구, w))
                break
        else:
            묶음.append([(경로, 문구, w)])

    되풀이 = sorted((g for g in 묶음 if len(g) > 1),
                    key=lambda g: -sum(len(t) for _, t, _ in g))
    총 = sum(len(t) for g in 되풀이 for _, t, _ in g)
    남길수 = sum(max(len(t) for _, t, _ in g) for g in 되풀이)
    print(f"── 되풀이 묶음 {len(되풀이)}개 · 합계 {총:,}자 "
          f"(한 번씩만 두면 {남길수:,}자, **{총 - 남길수:,}자 겹침**) ──\n")

    for i, g in enumerate(되풀이[:12], 1):
        print(f"[{i}] {len(g)}번 · {sum(len(t) for _, t, _ in g):,}자")
        for 경로, 문구, _ in g:
            print(f"    {경로}")
            print(f"      {문구[:130]}")
        print()

    # 자리별 — 어느 필드가 되풀이에 많이 걸리나
    자리 = defaultdict(int)
    for g in 되풀이:
        for 경로, 문구, _ in g:
            자리[경로.split(".")[0].split("[")[0]] += len(문구)
    print("── 되풀이가 몰린 자리 ──")
    for k, v in sorted(자리.items(), key=lambda x: -x[1]):
        print(f"  {v:>6,}자  {k}")

    print("\n★이 도구는 좋고 나쁨을 말하지 않는다. 되풀이가 **강조로 작동할**")
    print(" 수도 있다 — 08-24 에 「설명 걷기」가 기각된 적이 있다.")
    print(" 걷을지는 이 표를 보고 사람이 정한다.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
