"""발송 프롬프트 지표 — 전수 측정 (표본 외삽 없음).

Codex 검토(2026-08-15) 반영: 표본 8건 교집합 × 전체 호출 외삽을 버리고
전 호출을 직접 센다. '정적'은 줄 빈도(그 스텝 호출 중 해당 줄을 실은
비율) >= STATIC_DF 로 정의한다. 판정은 전부 구조 지표 — 어휘·의미 기반
판단 없음.

측정 정의(리포트 라벨과 일치해야 한다):
- 크기 단위는 전부 **자(Unicode code point)** — wire byte·token 아님.
- 같은 줄이 한 호출 안에 여러 번 있어도 1회로 센다(줄 존재 여부 기준).
- trace 1건 = 발송 1회로 세지 않는다 — provider retry 는 이 층에 없다.
"""
from collections import Counter, defaultdict
from typing import Any, Dict, List, Optional

from .fetch import messages_of, step_of, step_of_call

STATIC_DF = 0.9          # 이 비율 이상의 호출에 실린 줄 = 정적(템플릿)
CROSS_STEP_MIN_LINE = 40  # 교차 중복으로 셀 최소 줄 길이(관용구 소음 차단)


def cluster_of(span: Dict[str, Any]) -> str:
    """스텝 **안에서** 「같은 모양의 호출」을 가르는 키.

    ★스텝만으로 묶으면 안 된다(2026-08-25 실측). 한 스텝 안에 성격이 다른
    호출이 섞이면 서로의 줄 빈도를 깎아 **어느 줄도 `STATIC_DF` 문턱을 못
    넘고, 정적분이 통째로 0 으로 잡힌다.**

    실측 두 건:
    - `scene_detail` 10호출 = 본 호출 6(pro) + 보조 판정 4(flash).
      6/10=60%, 4/10=40% 라 둘 다 탈락 → 0.00. 군집을 나눠 재니
      system 의 **99.1%** 가 정적이었다(재전송 196,152자).
    - `scene_image_pipeline`(발송량 54%) 105호출 = roll·judge·critique·
      fix·signage 등 **12종류 이상**. 역시 0.00 이었다.

    가르는 것은 `op:` 축 태그 + 모델이다 — 그 둘이 호출의 성격을 말한다.
    """
    tags = span.get("tags") or []
    op = next((str(t)[3:] for t in tags if str(t).startswith("op:")), "")
    return f"{op}|{span.get('model') or ''}"


def per_step(traces: List[Dict[str, Any]],
             trace_index: Optional[Dict[str, Dict[str, Any]]] = None,
             ) -> Dict[str, Dict[str, Any]]:
    """스텝별 전수 집계.

    ★`trace_index` 를 주면 행을 **span** 으로 보고 부모에서 스텝을 얻는다
    (2026-08-24). 안 주면 예전처럼 행 자체에서만 얻는다 — 지난 자료 호환.

    반환 row:
      calls, {system,user}_avg, {system,user}_total(전수 합),
      {system,user}_static_ratio(정적 줄 글자 합/평균 크기),
      {system,user}_static_lines([(줄, df)] 길이순),
      total_chars(sys+usr 전수 합)
    """
    acc: Dict[str, Dict[str, Any]] = {}
    for t in traces:
        step = (step_of_call(t, trace_index)
                if trace_index is not None else step_of(t))
        msgs = messages_of(t)
        a = acc.setdefault(step, {
            "calls": 0,
            "system_total": 0, "user_total": 0,
            "clusters": {},
        })
        a["calls"] += 1
        cl = a["clusters"].setdefault(cluster_of(t), {
            "calls": 0,
            "system_df": Counter(), "user_df": Counter(),
        })
        cl["calls"] += 1
        for role in ("system", "user"):
            text = msgs[role]
            a[f"{role}_total"] += len(text)
            lines = set(text.splitlines())
            lines.discard("")
            cl[f"{role}_df"].update(lines)

    result: Dict[str, Dict[str, Any]] = {}
    for step, a in acc.items():
        n = a["calls"]
        row: Dict[str, Any] = {"calls": n, "clusters": len(a["clusters"])}
        for role in ("system", "user"):
            total = a[f"{role}_total"]
            avg = total // n if n else 0
            # ★군집마다 따로 잰다. 스텝 하나로 묶으면 성격이 다른 호출이
            #   서로의 빈도를 깎아 어느 줄도 문턱을 못 넘는다(위 docstring).
            static_weighted = 0   # 전 호출에 실린 정적 글자의 합
            resend = 0            # 그중 두 번째 호출부터 = 실제 낭비
            merged: Dict[str, int] = {}
            for cl in a["clusters"].values():
                cn = cl["calls"]
                if cn < 2:
                    continue      # 비교 대상이 없는 군집은 정적으로 안 센다
                static = [(line, c) for line, c in cl[f"{role}_df"].items()
                          if c >= STATIC_DF * cn]
                chars = sum(len(line) for line, _ in static)
                static_weighted += chars * cn
                resend += chars * (cn - 1)
                for line, c in static:
                    merged[line] = max(merged.get(line, 0), c)
            row[f"{role}_total"] = total
            row[f"{role}_avg"] = avg
            row[f"{role}_static_chars"] = (
                static_weighted // n if n else 0)   # 호출 하나당 정적 글자
            row[f"{role}_resend_chars"] = resend
            row[f"{role}_static_ratio"] = (
                round(static_weighted / total, 2) if total and n >= 2 else None)
            row[f"{role}_static_lines"] = sorted(
                merged.items(), key=lambda lc: -len(lc[0]))
        row["total_chars"] = row["system_total"] + row["user_total"]
        row["resend_chars"] = (row["system_resend_chars"]
                               + row["user_resend_chars"])
        result[step] = row
    return result


def cross_step_duplicates(
    steps: Dict[str, Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """여러 스텝의 정적 줄에 동시에 나타나는 문장.

    role 을 보존한다 — 시나리오 전문처럼 여러 스텝이 **공유하는 입력
    데이터**(user)와 **중복 주입된 규칙**(system)은 다른 부류다. 실증:
    v1 이 줄거리 전문을 '중복 조각 2위'로 잡았다(Codex 검토). 의미 판단은
    도구가 하지 않는다 — role 조합을 표기해 읽는 사람이 가른다.

    노출량 = 줄 길이 × Σ(각 스텝에서 그 줄을 실제로 실은 호출 수 df) —
    표본 외삽이 아니라 전수 df 다.
    """
    line_hits: Dict[str, List] = defaultdict(list)
    for step, row in steps.items():
        for role in ("system", "user"):
            for line, df in row.get(f"{role}_static_lines") or []:
                if len(line) >= CROSS_STEP_MIN_LINE:
                    line_hits[line].append((step, role, df))
    dups = []
    for line, hits in line_hits.items():
        step_set = {s for s, _, _ in hits}
        if len(step_set) < 2:
            continue
        dups.append({
            "line": line,
            "steps": sorted(step_set),
            "step_count": len(step_set),
            "roles": sorted({r for _, r, _ in hits}),
            "exposure_chars": len(line) * sum(df for _, _, df in hits),
        })
    dups.sort(key=lambda d: d["exposure_chars"], reverse=True)
    return dups
