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

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

from .fetch import messages_of, step_of

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


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

    반환 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(t)
        msgs = messages_of(t)
        a = acc.setdefault(step, {
            "calls": 0,
            "system_total": 0, "user_total": 0,
            "system_df": Counter(), "user_df": Counter(),
        })
        a["calls"] += 1
        for role in ("system", "user"):
            text = msgs[role]
            a[f"{role}_total"] += len(text)
            lines = set(text.splitlines())
            lines.discard("")
            a[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}
        for role in ("system", "user"):
            total = a[f"{role}_total"]
            avg = total // n if n else 0
            df: Counter = a[f"{role}_df"]
            static = [(line, c) for line, c in df.items()
                      if c >= STATIC_DF * n]
            static_chars = sum(len(line) for line, _ in static)
            row[f"{role}_total"] = total
            row[f"{role}_avg"] = avg
            row[f"{role}_static_ratio"] = (
                round(static_chars / avg, 2) if avg and n >= 2 else None)
            row[f"{role}_static_lines"] = sorted(
                static, key=lambda lc: -len(lc[0]))
        row["total_chars"] = row["system_total"] + row["user_total"]
        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
