# -*- coding: utf-8 -*-
"""utils.prompt_byte_classifier — task-2645 cron prompt UTF-8 byte gate.

회장 verbatim policy (2026-05-24 task-2644 사고 박제 §3 정확 정책):
- ≤3200 bytes        => OK_TARGET
- 3201~3499 bytes    => OK_ABOVE_TARGET
- 3500~3900 bytes    => WARNING_BUT_ALLOWED
- >3900 bytes        => HARD_BLOCK
- 4096 bytes         => cokacdir channel absolute limit (envelope max)
- 측정 단위: ``printf '%s' "$P" | wc -c`` 와 동일 (UTF-8 bytes, NOT chars)

>3900 bytes 프롬프트는 자동 path-only compact prompt 모드로 전환되어야 한다
(task md 경로 + sha256 만 envelope에 담는다). 이 모듈은 측정/분류/compact 변환
helper 를 제공하고, dispatch 측이 fail-closed gate 에서 호출한다.

본 모듈은 ANCHOR-2 (회장 verbatim 박제 단일소스 §3 정책) 의 코드화이다.
"""
from __future__ import annotations

import hashlib
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


# ── 박제 상수 (회장 verbatim — paraphrase 금지) ─────────────────────────────
# wc -c 측정 기준 UTF-8 bytes.
PROMPT_OK_TARGET_MAX: int = 3200
PROMPT_OK_ABOVE_TARGET_MAX: int = 3499
PROMPT_WARNING_MAX: int = 3900            # ≤3900 = warning + 발사 허용
PROMPT_HARD_BLOCK_THRESHOLD: int = 3900   # >3900 = HARD_BLOCK
# cokacdir 채널 absolute envelope limit. 4096 초과는 silent drop 위험 (673AA5A6 박제).
COKACDIR_CHANNEL_ABSOLUTE_LIMIT: int = 4096

# Verdict labels (회장 verbatim 4 구간 + absolute) — JSON schema와 동일 token.
OK_TARGET = "OK_TARGET"
OK_ABOVE_TARGET = "OK_ABOVE_TARGET"
WARNING_BUT_ALLOWED = "WARNING_BUT_ALLOWED"
HARD_BLOCK = "HARD_BLOCK"
CHANNEL_ABSOLUTE_OVERFLOW = "CHANNEL_ABSOLUTE_OVERFLOW"

VERDICTS_ALLOWED = frozenset({OK_TARGET, OK_ABOVE_TARGET, WARNING_BUT_ALLOWED})
VERDICTS_BLOCKED = frozenset({HARD_BLOCK, CHANNEL_ABSOLUTE_OVERFLOW})


@dataclass(frozen=True)
class PromptByteClassification:
    """단일 prompt 분류 결과 (immutable; JSON schema 1:1)."""

    utf8_bytes: int
    verdict: str
    allowed: bool
    warning: bool
    hard_block: bool
    channel_overflow: bool
    requires_compact_mode: bool
    reason: str

    def to_json(self) -> dict:
        return {
            "schema": "dispatch.prompt_byte_classification.v1",
            "utf8_bytes": self.utf8_bytes,
            "verdict": self.verdict,
            "allowed": self.allowed,
            "warning": self.warning,
            "hard_block": self.hard_block,
            "channel_overflow": self.channel_overflow,
            "requires_compact_mode": self.requires_compact_mode,
            "reason": self.reason,
            "policy": {
                "ok_target_max": PROMPT_OK_TARGET_MAX,
                "ok_above_target_max": PROMPT_OK_ABOVE_TARGET_MAX,
                "warning_max": PROMPT_WARNING_MAX,
                "hard_block_threshold": PROMPT_HARD_BLOCK_THRESHOLD,
                "channel_absolute_limit": COKACDIR_CHANNEL_ABSOLUTE_LIMIT,
            },
        }


def measure_utf8_bytes(prompt: object) -> int:
    """``printf '%s' "$prompt" | wc -c`` 와 동등한 UTF-8 byte count.

    ★ wc -c bytes — NOT wc -m chars. ANU paraphrase 사고 (≤2800자 권장을 hard limit
    으로 해석) 의 정정 doctrine이다. caller 타입 미신뢰 — None / non-str 도 안전 처리.
    """
    if prompt is None:
        return 0
    if not isinstance(prompt, str):
        raise TypeError(f"prompt must be str, got {type(prompt).__name__}")
    return len(prompt.encode("utf-8"))


def classify_prompt_bytes(prompt: str) -> PromptByteClassification:
    """회장 verbatim 4 구간 + 4096 absolute 분류."""
    n = measure_utf8_bytes(prompt)
    return classify_byte_count(n)


def classify_byte_count(n: int) -> PromptByteClassification:
    """byte count 만으로 분류 (이미 측정된 값 재분류 용도)."""
    if n < 0:
        raise ValueError(f"byte count must be >= 0, got {n}")

    # 4096 절대 한도 초과 — 673AA5A6 silent drop 박제 케이스.
    if n > COKACDIR_CHANNEL_ABSOLUTE_LIMIT:
        return PromptByteClassification(
            utf8_bytes=n,
            verdict=CHANNEL_ABSOLUTE_OVERFLOW,
            allowed=False,
            warning=False,
            hard_block=True,
            channel_overflow=True,
            requires_compact_mode=True,
            reason=(
                f"prompt {n} bytes > {COKACDIR_CHANNEL_ABSOLUTE_LIMIT} "
                f"(cokacdir channel absolute limit) — silent drop 위험."
            ),
        )

    # >3900 hard block (>=4096 인 경우는 위 분기로 이미 처리됨).
    if n > PROMPT_HARD_BLOCK_THRESHOLD:
        return PromptByteClassification(
            utf8_bytes=n,
            verdict=HARD_BLOCK,
            allowed=False,
            warning=False,
            hard_block=True,
            channel_overflow=False,
            requires_compact_mode=True,
            reason=(
                f"prompt {n} bytes > {PROMPT_HARD_BLOCK_THRESHOLD} (HARD_BLOCK). "
                "path-only compact mode 로 자동 전환 필요."
            ),
        )

    # 3500 ~ 3900 warning + 발사 허용.
    if n >= 3500:
        return PromptByteClassification(
            utf8_bytes=n,
            verdict=WARNING_BUT_ALLOWED,
            allowed=True,
            warning=True,
            hard_block=False,
            channel_overflow=False,
            requires_compact_mode=False,
            reason=(
                f"prompt {n} bytes (3500~{PROMPT_WARNING_MAX}) — 압축 권장, 발사 허용."
            ),
        )

    # 3201 ~ 3499 OK_ABOVE_TARGET.
    if n >= 3201:
        return PromptByteClassification(
            utf8_bytes=n,
            verdict=OK_ABOVE_TARGET,
            allowed=True,
            warning=False,
            hard_block=False,
            channel_overflow=False,
            requires_compact_mode=False,
            reason=f"prompt {n} bytes (3201~{PROMPT_OK_ABOVE_TARGET_MAX}) — 권장 target 약간 초과, 안전.",
        )

    # ≤3200 OK_TARGET.
    return PromptByteClassification(
        utf8_bytes=n,
        verdict=OK_TARGET,
        allowed=True,
        warning=False,
        hard_block=False,
        channel_overflow=False,
        requires_compact_mode=False,
        reason=f"prompt {n} bytes ≤ {PROMPT_OK_TARGET_MAX} — 권장 target.",
    )


def is_allowed(prompt: str) -> bool:
    """발사 허용 여부 (allowed=True 이면 발사 진행, 단 warning 은 압축 권장 별도 로깅)."""
    return classify_prompt_bytes(prompt).allowed


def requires_path_only_compact(prompt: str) -> bool:
    """>3900 bytes 또는 4096 초과 — path-only compact mode 강제 여부."""
    return classify_prompt_bytes(prompt).requires_compact_mode


def build_path_only_compact_prompt(
    task_md_path: str | Path,
    *,
    extra_context: Optional[str] = None,
) -> str:
    """>3900 bytes 자동 path-only compact prompt 생성.

    회장 verbatim §17 항목 7: "prompt >3900 시 자동 path-only compact prompt mode
    (task md 경로 + sha256 만)".

    Args:
        task_md_path: dispatched task md 절대/상대 경로 (문자열 또는 Path).
        extra_context: 선택. 1줄 추가 컨텍스트 (envelope 안에 헬퍼 caller 가 합쳐 넣을 때).

    Returns:
        compact prompt 문자열. envelope 합산 후에도 ≤3900 bytes 가 되도록 envelope
        caller 가 검증해야 한다 (helper 단독은 task md 경로 + sha256 + 1줄 지시).
    """
    p = Path(task_md_path)
    sha = "MISSING"
    try:
        if p.is_file():
            with open(p, "rb") as f:
                sha = hashlib.sha256(f.read()).hexdigest()
    except OSError:
        sha = "MISSING"

    lines = [
        "[DISPATCH_PATH_ONLY_COMPACT]",
        f"task_md_path: {p}",
        f"task_md_sha256: {sha}",
        "instruction: 위 경로 task md 정독 후 즉시 시작. 본 envelope 은 path-only compact (>3900 bytes 자동 전환).",
    ]
    if extra_context:
        lines.append(f"extra: {extra_context.strip()[:200]}")
    return "\n".join(lines)


__all__ = [
    "PROMPT_OK_TARGET_MAX",
    "PROMPT_OK_ABOVE_TARGET_MAX",
    "PROMPT_WARNING_MAX",
    "PROMPT_HARD_BLOCK_THRESHOLD",
    "COKACDIR_CHANNEL_ABSOLUTE_LIMIT",
    "OK_TARGET",
    "OK_ABOVE_TARGET",
    "WARNING_BUT_ALLOWED",
    "HARD_BLOCK",
    "CHANNEL_ABSOLUTE_OVERFLOW",
    "VERDICTS_ALLOWED",
    "VERDICTS_BLOCKED",
    "PromptByteClassification",
    "measure_utf8_bytes",
    "classify_prompt_bytes",
    "classify_byte_count",
    "is_allowed",
    "requires_path_only_compact",
    "build_path_only_compact_prompt",
]
