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

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

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

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

PR4-B4 carry — this script is **CP-free**: it reads prompt files directly
via filesystem and does NOT import the render-strategy snapshot hash
helper. The `render_strategy_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.

Trap #1 silent-fallback ban — applies to pinning loading; tiktoken is hard
dependency (no fallback).

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

Usage (config mode):
    python scripts/canary/g4_5a_token_count.py \\
        --baseline-version 19.202605050814 \\
        --candidate-version 20.202605051240 \\
        --output results/g4_5a_token_count.json
"""
from __future__ import annotations

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

# G4.5a R3-M2 carry (PR4-B4 binding) — token script CP-free, NO render-strategy
# snapshot hash import. Pinning fields are echoed only; pinning is NOT
# cross-validated against CP. analyzer cross-checks against 4 metric scripts'
# output.

# We intentionally do NOT import from `_g4_5a_common` because that module
# loads `render_prompt_card` (transitively the snapshot hash helper) at import
# time via `render_prompt_card_imports()` — PR4-B4 ban.

# Sentinel for explicit-None semantics — Trap #1 / PR-fix-iter-1-1 strict.
_DEFAULT_SENTINEL = object()


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


def _load_pinning_from_args(args: argparse.Namespace) -> dict:
    """Load pinning dict from --config OR individual flags.

    Trap #1 — `_DEFAULT_SENTINEL` is the only allowed default; absent keys
    round-trip as None and `_validate_pinning()` rejects them.
    """
    if getattr(args, "config", None):
        cfg_path = Path(args.config)
        if not cfg_path.exists():
            print(
                f"[g4_5a_token_count] ERROR: --config not found: {cfg_path}",
                file=sys.stderr,
            )
            sys.exit(1)
        try:
            with cfg_path.open("r", encoding="utf-8") as fp:
                cfg = json.load(fp)
        except Exception as exc:  # noqa: BLE001
            print(
                f"[g4_5a_token_count] ERROR: --config parse error: {exc}",
                file=sys.stderr,
            )
            sys.exit(1)

        def _cfg_or_none(key: str):
            val = cfg.get(key, _DEFAULT_SENTINEL)
            return None if val is _DEFAULT_SENTINEL else val

        return {
            "pid": _cfg_or_none("pid"),
            "scene_index_list": _cfg_or_none("scene_index_list"),
            "shot_index_list_per_scene": _cfg_or_none(
                "shot_index_list_per_scene"
            ),
            "model_routing": _cfg_or_none("model_routing"),
            "prompt_source_mode": _cfg_or_none("prompt_source_mode"),
            "card_commit_hash": _cfg_or_none("card_commit_hash"),
            "render_strategy_card_snapshot_hash": _cfg_or_none(
                "render_strategy_card_snapshot_hash"
            ),
        }
    try:
        scene_index_list = (
            json.loads(args.scene_index_list)
            if getattr(args, "scene_index_list", None)
            else None
        )
        shot_index_list_per_scene = (
            json.loads(args.shot_index_list_per_scene)
            if getattr(args, "shot_index_list_per_scene", None)
            else None
        )
    except Exception as exc:  # noqa: BLE001
        print(
            f"[g4_5a_token_count] ERROR: failed to parse JSON args: {exc}",
            file=sys.stderr,
        )
        sys.exit(1)
    return {
        "pid": getattr(args, "pid", None),
        "scene_index_list": scene_index_list,
        "shot_index_list_per_scene": shot_index_list_per_scene,
        "model_routing": getattr(args, "model_routing", None),
        "prompt_source_mode": getattr(args, "prompt_source_mode", None),
        "card_commit_hash": getattr(args, "card_commit_hash", None),
        "render_strategy_card_snapshot_hash": getattr(
            args, "render_strategy_card_snapshot_hash", None
        ),
    }


def _emit_output(out: dict, args: argparse.Namespace) -> None:
    """Write JSON output to args.output ('-' for stdout)."""
    if args.output == "-":
        json.dump(out, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
        sys.stdout.write("\n")
    else:
        out_path = Path(args.output)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        with out_path.open("w", encoding="utf-8") as fp:
            json.dump(out, fp, ensure_ascii=False, indent=2, sort_keys=True)


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "G4.5a canary: token-count delta (system.md, tiktoken cl100k_base, "
            "PR4-B4 CP-free, Trap #6 signed)."
        )
    )
    parser.add_argument(
        "--config",
        type=str,
        default=None,
        help=(
            "Optional JSON config file. If provided, overrides individual "
            "--baseline-version / --candidate-version / --pid / etc. flags."
        ),
    )
    parser.add_argument(
        "--baseline-version",
        type=str,
        required=True,
        help="Baseline prompt version directory name (e.g. 19.202605050814)",
    )
    parser.add_argument(
        "--candidate-version",
        type=str,
        required=True,
        help="Candidate prompt version directory name (e.g. 20.<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=-1000,
        help=(
            "token_delta upper bound for pass "
            "(default: -1000, RO-1 binding)"
        ),
    )
    parser.add_argument(
        "--system-plus-inject-secondary-threshold",
        type=int,
        default=-800,
        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.5a — 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(
        "--render-strategy-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)
    # PR4-B4 carry: pinning is echoed without strict validation here — the 4
    # metric scripts validate pinning + CP cross-check; token script trusts
    # operator input. analyzer compares pinning across 5 outputs.

    measurement_failures: list[str] = []

    try:
        import tiktoken  # type: ignore
    except ImportError:
        # Trap #1 — silent fallback ban. tiktoken absence = 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_5a_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_5a_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_5a_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_5a_token_count] ERROR: tiktoken encode failed: {exc}",
            file=sys.stderr,
        )
        return 1

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

    # Optional secondary report-only metric (RO-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_5a_token_count.py "
                "(system.md, tiktoken cl100k_base, PR4-B4 CP-free, "
                "Trap #6 signed, RO-1 -1000 gate)"
            )
        },
    }

    _emit_output(out, args)

    print(
        f"[g4_5a_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_5a_token_count.py "
                "(system.md, tiktoken cl100k_base, PR4-B4 CP-free)"
            )
        },
    }


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