#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""G4.4 canary — token-count delta (system.md, tiktoken cl100k_base) (Task 4.5).

Computes baseline (v18) and candidate (v19) token counts for
`prompts/_base/scene_detail/<version>/system.md` and emits the signed delta.

Override O-1 binding gate (signed convention):
  token_delta = candidate_token_count - baseline_token_count
  pass condition: token_delta <= -540  (system-only)
which is mathematically equivalent to baseline - candidate >= 540.

`system_plus_inject_delta <= -150` = secondary report-only metric (no gate).

R3-M2 carry — this script is **CP-free**: it reads prompt files directly
via filesystem and does NOT import the continuity snapshot-hash helper. The
`continuity_card_snapshot_hash` echoed in pinning is operator-supplied and
verified by analyzer cross-check against the 4 metric scripts.

Trap #6 — token gate signed: candidate < baseline ⇒ delta is negative.

Exit criteria (analyzer):
  exit_criteria_pass == True  iff  token_delta <= -540  (default threshold)

Usage (config mode):
    python scripts/canary/g4_4_token_count.py \\
        --config canary_config.json \\
        --baseline-version 18.202605041549 \\
        --candidate-version 19.<timestamp> \\
        --output results/g4_4_token_count.json
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
from _g4_4_common import (  # noqa: E402
    emit_output,
    load_pinning_from_args,
    validate_pinning,
)


def iso8601_now() -> str:
    return datetime.utcnow().isoformat() + "Z"


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "G4.4 canary: token-count delta (system.md, "
            "tiktoken cl100k_base, R3-M2 CP-free)."
        )
    )
    parser.add_argument(
        "--config",
        type=str,
        default=None,
        help=(
            "Optional JSON config file path. If provided, overrides "
            "individual --baseline-version / --candidate-version / --pid / "
            "etc. flags. Mutually exclusive with explicit flags."
        ),
    )
    parser.add_argument(
        "--baseline-version",
        type=str,
        required=True,
        help="Baseline prompt version directory name "
        "(e.g. 18.202605041549)",
    )
    parser.add_argument(
        "--candidate-version",
        type=str,
        required=True,
        help="Candidate prompt version directory name (e.g. 19.<timestamp>)",
    )
    parser.add_argument(
        "--prompt-base-path",
        type=str,
        default="prompts/_base/scene_detail",
        help="Base directory containing version subdirs",
    )
    parser.add_argument(
        "--exit-criteria-threshold",
        type=int,
        default=-540,
        help=(
            "token_delta upper bound for pass "
            "(default: -540, Override O-1 binding)"
        ),
    )
    parser.add_argument(
        "--system-plus-inject-secondary-threshold",
        type=int,
        default=-150,
        help=(
            "system+inject combined delta secondary threshold "
            "(report-only, no gate)"
        ),
    )
    parser.add_argument(
        "--baseline-system-plus-inject",
        type=int,
        default=None,
        help=(
            "Optional baseline system+inject token count "
            "(operator-supplied, secondary report-only)"
        ),
    )
    parser.add_argument(
        "--candidate-system-plus-inject",
        type=int,
        default=None,
        help=(
            "Optional candidate system+inject token count "
            "(operator-supplied, secondary report-only)"
        ),
    )
    # Pinning fields echoed for cross-canary consistency (G4.4 — 7 fields).
    parser.add_argument("--pid", type=str, default=None)
    parser.add_argument("--scene-index-list", type=str, default=None)
    parser.add_argument("--shot-index-list-per-scene", type=str, default=None)
    parser.add_argument("--model-routing", type=str, default=None)
    parser.add_argument(
        "--prompt-source-mode",
        type=str,
        default=None,
        choices=[None, "file", "db"],
    )
    parser.add_argument("--card-commit-hash", type=str, default=None)
    parser.add_argument(
        "--continuity-card-snapshot-hash", type=str, default=None
    )
    parser.add_argument("--output", type=str, required=True)
    args = parser.parse_args()

    pinning = load_pinning_from_args(args)
    validate_pinning(pinning)
    # R3-M2 carry: 본 스크립트는 prompt 파일만 읽고 CP 를 load 하지 않으므로
    # `continuity_card_snapshot_hash` 값은 검증 없이 echo 만 한다 (operator
    # 입력 신뢰). 4 metric 스크립트들은 같은 hash 를 CP 에서 직접 산출/검증
    # 하므로, token 스크립트의 echo 와 4 metric 의 산출 hash 가 다르면
    # analyzer 단계에서 불일치 catch.

    measurement_failures: list[str] = []

    try:
        import tiktoken  # type: ignore
    except ImportError:
        # Trap #1 — silent fallback ban. tiktoken 부재는 fail-fast.
        msg = (
            "tiktoken not installed. "
            "Install via `pip install tiktoken` (or backend venv). "
            "NO silent fallback."
        )
        measurement_failures.append(msg)
        out = _failure_output(args, pinning, measurement_failures)
        emit_output(out, args)
        print(f"[g4_4_token_count] ERROR: {msg}", file=sys.stderr)
        return 1

    encoder = tiktoken.get_encoding("cl100k_base")  # type: ignore[name-defined]

    base_dir = Path(args.prompt_base_path)
    baseline_path = base_dir / args.baseline_version / "system.md"
    candidate_path = base_dir / args.candidate_version / "system.md"

    # Validate prompt files exist BEFORE reading (silent-absorb 차단).
    if not baseline_path.exists():
        measurement_failures.append(
            f"baseline system.md not found: {baseline_path}"
        )
    if not candidate_path.exists():
        measurement_failures.append(
            f"candidate system.md not found: {candidate_path}"
        )
    if measurement_failures:
        out = _failure_output(args, pinning, measurement_failures)
        emit_output(out, args)
        for msg in measurement_failures:
            print(f"[g4_4_token_count] ERROR: {msg}", file=sys.stderr)
        return 1

    try:
        baseline_text = baseline_path.read_text(encoding="utf-8")
        candidate_text = candidate_path.read_text(encoding="utf-8")
    except Exception as exc:  # noqa: BLE001
        measurement_failures.append(f"prompt file read failed: {exc}")
        out = _failure_output(args, pinning, measurement_failures)
        emit_output(out, args)
        print(
            f"[g4_4_token_count] ERROR: prompt file read failed: {exc}",
            file=sys.stderr,
        )
        return 1

    try:
        baseline_token_count = len(encoder.encode(baseline_text))
        candidate_token_count = len(encoder.encode(candidate_text))
    except Exception as exc:  # noqa: BLE001
        measurement_failures.append(f"tiktoken encode failed: {exc}")
        out = _failure_output(args, pinning, measurement_failures)
        emit_output(out, args)
        print(
            f"[g4_4_token_count] ERROR: tiktoken encode failed: {exc}",
            file=sys.stderr,
        )
        return 1

    # Trap #6 — signed delta convention. Negative delta = reduction.
    # Threshold -540 means candidate must be at least 540 tokens shorter.
    token_delta = candidate_token_count - baseline_token_count
    exit_pass = token_delta <= args.exit_criteria_threshold

    # Optional secondary report-only metric (Override O-1).
    if (
        args.baseline_system_plus_inject is not None
        and args.candidate_system_plus_inject is not None
    ):
        spi_baseline = args.baseline_system_plus_inject
        spi_candidate = args.candidate_system_plus_inject
        spi_delta = spi_candidate - spi_baseline
        spi_secondary_pass = (
            spi_delta <= args.system_plus_inject_secondary_threshold
        )
    else:
        spi_baseline = None
        spi_candidate = None
        spi_delta = None
        spi_secondary_pass = None

    out = {
        "timestamp": iso8601_now(),
        "pinning": pinning,
        "baseline_version": args.baseline_version,
        "candidate_version": args.candidate_version,
        "baseline_token_count": baseline_token_count,
        "candidate_token_count": candidate_token_count,
        "token_delta": token_delta,
        "exit_criteria_threshold": args.exit_criteria_threshold,
        "exit_criteria_pass": exit_pass,
        "system_plus_inject_baseline": spi_baseline,
        "system_plus_inject_candidate": spi_candidate,
        "system_plus_inject_delta": spi_delta,
        "system_plus_inject_secondary_threshold": (
            args.system_plus_inject_secondary_threshold
        ),
        "system_plus_inject_secondary_pass": spi_secondary_pass,
        "measurement_tool": "tiktoken cl100k_base",
        "measurement_failures": measurement_failures,
        "measurement_scripts": {
            "token_count": (
                "scripts/canary/g4_4_token_count.py "
                "(system.md, tiktoken cl100k_base, R3-M2 CP-free, "
                "Trap #6 signed)"
            )
        },
    }

    emit_output(out, args)

    print(
        f"[g4_4_token_count]\n"
        f"  baseline ({args.baseline_version}): "
        f"{baseline_token_count} tokens\n"
        f"  candidate ({args.candidate_version}): "
        f"{candidate_token_count} tokens\n"
        f"  token_delta: {token_delta}  (signed: candidate - baseline)\n"
        f"  threshold: {args.exit_criteria_threshold}  "
        f"(pass iff token_delta <= threshold)\n"
        f"  exit_criteria_pass: {exit_pass}\n"
        f"  system_plus_inject_delta: {spi_delta} "
        f"(secondary report-only)",
        file=sys.stderr,
    )
    return 0 if exit_pass else 1


def _failure_output(
    args: argparse.Namespace,
    pinning: dict,
    measurement_failures: list,
) -> dict:
    return {
        "timestamp": iso8601_now(),
        "pinning": pinning,
        "baseline_version": args.baseline_version,
        "candidate_version": args.candidate_version,
        "baseline_token_count": None,
        "candidate_token_count": None,
        "token_delta": None,
        "exit_criteria_threshold": args.exit_criteria_threshold,
        "exit_criteria_pass": False,
        "system_plus_inject_baseline": None,
        "system_plus_inject_candidate": None,
        "system_plus_inject_delta": None,
        "system_plus_inject_secondary_threshold": (
            args.system_plus_inject_secondary_threshold
        ),
        "system_plus_inject_secondary_pass": None,
        "measurement_tool": "tiktoken cl100k_base",
        "measurement_failures": measurement_failures,
        "measurement_scripts": {
            "token_count": (
                "scripts/canary/g4_4_token_count.py "
                "(system.md, tiktoken cl100k_base, R3-M2 CP-free)"
            )
        },
    }


if __name__ == "__main__":
    sys.exit(main())
