#!/usr/bin/env python3
"""
Model Router hook (advisory mode) — classifier v2 (weighted scoring).

Pipeline:
  1. Collect rule matches as (tier, weight) pairs.
  2. Apply scoring modifiers:
       - BULK_INDICATORS boost CHEAP when paired with a cheap-verb match.
       - BOUNDED_INDICATORS suppress HARD when present, lift MAIN.
       - NEGATION preceding a HARD keyword cancels that HARD weight,
         lifts CHEAP by a small amount.
  3. Decide: highest total wins; ties resolve toward HARD (escalation-biased
     — false-Opus is cheap insurance vs false-Haiku). All-zero → MAIN default.

Mode: ADVISORY. Logs every decision to /tmp/model-router.log. Hook MUST
never break a session — all I/O wrapped in try/except.
"""

import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

LOG_PATH = Path("/tmp/model-router.log")

# Each rule: (tier, weight, regex_pattern)
RULES = [
    # ============================================================
    # CHEAP — mechanical / deterministic transforms
    # ============================================================
    ("cheap", 5, r"\brename\b"),
    ("cheap", 4, r"\bformat\s+(?:this|the|all|every|our)\b"),
    ("cheap", 4, r"\bformat\s+\S+\.(?:json|ya?ml|md|css|s?css|html|toml|sql|js|ts|tsx|jsx|py|rb|go|rs)\b"),
    ("cheap", 4, r"\bsort\s+(?:the\s+)?(?:entries|keys|values|list|imports|deps|devdependencies)\b"),
    ("cheap", 4, r"\bbump\s+(?:all\s+|every\s+)?(?:internal\s+)?\S+\s+(?:dep|deps|dependenc|version|engines)"),
    ("cheap", 4, r"\bbump\s+every\s+package\.json"),
    ("cheap", 5, r"\blist\s+(?:every|all)\s+(?:todo|fixme)"),
    ("cheap", 4, r"\btranslate\s+(?:every|the|this|all)\b"),
    ("cheap", 5, r"\badd\s+jsdoc\b"),
    ("cheap", 5, r"\badd\s+docstrings?\b"),
    ("cheap", 5, r"\badd\s+(?:precise\s+)?(?:typescript\s+)?type\s+annotations\b"),
    ("cheap", 4, r"\badd\s+explicit\s+return\s+types?\b"),
    ("cheap", 4, r"\bgenerate\s+(?:empty\s+)?[\w/,\s]{0,40}\bstubs?\b"),
    ("cheap", 5, r"\bupdate\s+(?:the\s+)?copyright\s+year\b"),
    ("cheap", 4, r"\bnpm\s+run\s+lint\s*--?\s*fix\b"),
    ("cheap", 5, r"\bprettier\s+--write\b"),
    ("cheap", 5, r"\beslint\s+--fix\b"),
    ("cheap", 5, r"\bbiome\s+format\b"),

    # Cheap "transform every X" verbs
    ("cheap", 3, r"\bconvert\b[^\n]{0,80}\b(?:to|from)\s+(?:async|await|camel|snake|kebab|pascal|f[-\s]string|format|template|sha256|sha512|esm|cjs|https?|json|yaml|screaming)"),
    ("cheap", 4, r"\bconvert\s+every\b"),
    ("cheap", 4, r"\breplace\s+every\b"),
    ("cheap", 4, r"\breplace\s+all\s+(?:hardcoded|inline|magic|the)\b"),
    ("cheap", 4, r"\bstrip\s+(?:every|all|trailing|leading)\b"),
    ("cheap", 4, r"\bdrop\s+(?:every|all|the\s+)?unused\b"),
    ("cheap", 4, r"\bswap\s+(?:every|all|them)\b"),
    ("cheap", 4, r"\brewrite\s+(?:the\s+)?(?:file[-\s]extension|imports?|requires?)\b"),
    ("cheap", 3, r"\binline\s+the\s+\S+\s+helper\b"),
    ("cheap", 4, r"\bstandardise\s+(?:the\s+)?(?:test\s+file\s+naming|imports?|file\s+headers?|json|trailing|newline)\b"),
    ("cheap", 5, r"\bun-?skip\b"),
    ("cheap", 4, r"\badd\s+(?:a\s+)?trailing\s+(?:newline|semicolon|comma)\b"),
    ("cheap", 5, r"\bfind\s+(?:and\s+)?replace\b"),
    ("cheap", 5, r"\bmass[-\s]substitute\b"),
    ("cheap", 4, r"\bpure\s+substitution\b"),
    ("cheap", 3, r"\balphabetic(?:al(?:ly)?)?\b"),
    ("cheap", 4, r"\bchange\s+every\b"),
    ("cheap", 4, r"\bupdate\s+every\s+(?:\S+\s+){0,2}(?:to|with|from)\b"),
    ("cheap", 3, r"\b(?:swap|change)\s+them\s+to\b"),
    ("cheap", 4, r"\bone[-\s]line\s+change\s+per\s+file\b"),
    ("cheap", 5, r"\bjust\s+rename\b"),
    ("cheap", 5, r"\bpurely\s+mechanical\b"),
    ("cheap", 5, r"\bno\s+(?:logic|behavioural?|behavioral|other)\s+changes?\b"),
    ("cheap", 5, r"\bpreserve\s+behaviou?r\s+exactly\b"),
    ("cheap", 4, r"\bfix\s+(?:the\s+)?relative\s+paths\b"),
    ("cheap", 4, r"\bremove\s+the\s+\.\w+\s+modifier\b"),
    # "every <thing>" + directive patterns (mass-operation prose)
    ("cheap", 5, r"\bfind\s+every\b"),
    ("cheap", 5, r"\bupdate\s+all\s+the\b"),
    ("cheap", 4, r"\bupdate\s+every\b"),
    ("cheap", 5, r"\badd\s+\S+(?:\s+\S+)?\s+(?:to|in)\s+every\b"),
    ("cheap", 5, r"\badd\s+(?:them|it|these)\s+(?:back|where\s+missing|here)\b"),
    ("cheap", 4, r"\badd\s+it\s+where\s+missing\b"),
    ("cheap", 4, r"\bstandardi[sz]e[\s:]"),
    ("cheap", 3, r"\bone[-\s]line\s+change\s+per\s+file\b"),
    ("cheap", 4, r"\b(?:dbt[-\s]style|fyi|cleanup\s+pass|cleanup\s+task)\s*[:—]"),
    ("cheap", 3, r"\bevery\s+(?:\.\w+\s+file|\S+\s+(?:file|files|component|components|import|imports|importer|module|modules|test|tests|header|headers|line|lines|string|strings|env|var|vars|variable|variables|key|keys))\b"),
    ("cheap", 3, r"\bevery\s+\S+\s+(?:should\s+be|should\s+have|should\s+end|needs\s+a?|that's\s+only)\b"),
    ("cheap", 4, r"\bevery\s+\S+\s*[→]\s*\S+"),
    ("cheap", 3, r"\bjust\s+the\s+year"),

    # ============================================================
    # MAIN — anti-CHEAP for bounded feature-adds
    # ============================================================
    ("main", 5, r"\badd\s+an?\s+(?:\S+\s+){0,3}(?:flag|field|route|endpoint|column|badge|button|hook|provider|middleware|option|feature\s+flag\s+check|rule|debounce|cache|progress\s+bar|modal|toast|notification)\b"),

    # ============================================================
    # HARD — architecture / migration / cross-service / deep investigation
    # ============================================================
    # Verbs / decision asks
    ("hard", 6, r"\bdesign\s+(?:a\s+|the\s+)?(?:feature[-\s]flag\s+system|migration|data\s+model|architecture|rollout|unified|API\s+versioning|backwards?[-\s]compatible)"),
    ("hard", 5, r"\bpropose\s+(?:an?\s+)?(?:architectural|backwards?[-\s]compatible|migration|deprecation|rollout|unified|strategy|path)"),
    ("hard", 5, r"\barchitect(?:ure)?\b"),
    ("hard", 5, r"\barchitect\s+the\s+fix\b"),
    ("hard", 4, r"\binvestigate\b"),
    ("hard", 5, r"\baudit\s+(?:all|every|across|retry|logging|all\s+\d+\s+services?)\b"),
    ("hard", 6, r"\bunify\b[^\n]{0,80}\bacross\b"),
    ("hard", 6, r"\brefactor\s+(?:cross[-\s]cutting|every\s+service)"),
    ("hard", 6, r"\brefactor\b[^\n]{0,80}\bacross\s+(?:every|all|the\s+monorepo)"),
    ("hard", 6, r"\bplan\s+(?:the\s+|a\s+)?(?:rollout|migration|deprecation|sharding|cutover|sunset|deploy|deployment)"),

    # Architectural concept nouns
    ("hard", 6, r"\bzero[-\s]downtime\b"),
    ("hard", 6, r"\bmulti[-\s]tenant\b"),
    ("hard", 6, r"\bsharding\b"),
    ("hard", 5, r"\brace\s+condition\b"),
    ("hard", 5, r"\bp99\b|\bp95\b|\bp999\b"),
    ("hard", 5, r"\blatency\s+spike"),
    ("hard", 5, r"\btail\s+latenc"),
    ("hard", 5, r"\bmemory\s+leak\b"),
    ("hard", 4, r"\btradeoffs?\b"),
    ("hard", 5, r"\bsaga\b"),
    ("hard", 5, r"\b2pc\b|\btwo[-\s]phase\s+commit\b"),
    ("hard", 6, r"\bmonorepo[-\s]wide\b"),
    ("hard", 5, r"\bstart\s+from\b[^\n]{0,100}\bend\s+with\b"),
    ("hard", 5, r"\bcross[-\s]service\b"),
    ("hard", 6, r"\bbackwards[-\s]compatible\s+(?:api|migration|strategy|versioning)"),
    ("hard", 4, r"\bbackwards[-\s]compatible\b"),
    ("hard", 5, r"\bmigrate\s+every\b"),
    ("hard", 4, r"\bevery\s+endpoint\b"),
    ("hard", 6, r"\bpool\s+exhaustion\b"),
    ("hard", 6, r"\bfeature[-\s]flag\s+system\b"),
    ("hard", 5, r"\bevery\s+(?:service|repo|package|microservice|public\s+endpoint|mutating\s+endpoint)\b"),
    ("hard", 4, r"\bcross[-\s]cutting\b"),
    ("hard", 5, r"\bunified\s+(?:middleware|layer|policy|contract|approach)\b"),
    ("hard", 6, r"\bcircular\s+depend"),
    ("hard", 6, r"\bdata\s+residency\b"),
    ("hard", 6, r"\baudit\s+log\b"),
    ("hard", 6, r"\bidempotency\s+layer\b"),
    ("hard", 6, r"\bevent\s+sourcing\b"),
    ("hard", 6, r"\bdrift\s+detection\b"),
    ("hard", 6, r"\bbottleneck\s+analysis\b"),
    ("hard", 5, r"\bpermanent\s+fix\b"),
    ("hard", 5, r"\bdual[-\s]write\b"),
    ("hard", 5, r"\bself[-\s]healing\b"),
    ("hard", 5, r"\bsoc\s?2\b"),
    ("hard", 4, r"\bpii\s+(?:write|data|field|column|store|storage|audit)\b"),
    ("hard", 5, r"\bttfb\b"),
    ("hard", 4, r"\bdata\s+model\b"),
    ("hard", 4, r"\bauth\s+systems?\b"),
    ("hard", 5, r"\bdeadlock"),
    ("hard", 5, r"\bdeploy\s+pipeline\b"),
    ("hard", 5, r"\bthroughput\s+stall"),
    ("hard", 4, r"\bdesync\b"),
    ("hard", 5, r"\bfile\s+descriptors?\b|\bfd\s+leak\b"),
    ("hard", 5, r"\boperationally\s+serious\b"),
    ("hard", 5, r"\breal[-\s]time\s+chat\b"),
    ("hard", 5, r"\binfra\s+(?:bill|cost)\b|\bci\s+cost\b"),
    ("hard", 5, r"\boncall\s+(?:is\s+)?(?:getting\s+)?paged\b"),
    ("hard", 4, r"\bdrift(?:ed)?\s+(?:in|from|across|over)\b"),
    ("hard", 5, r"\bstandardise\s+(?:the\s+)?(?:error|contract|response)\s+(?:across|and\s+migrate)"),
    ("hard", 5, r"\bstandardize\s+(?:the\s+)?(?:error|contract|response)\s+(?:across|and\s+migrate)"),

    # Cross-system / scale signals
    ("hard", 5, r"\b\d+\s+(?:packages|services|microservices|consumers|regions|bindings|tenants|language\s+bindings)\b"),
    ("hard", 5, r"\b\$\d+[Kk]?\s*/\s*month\b"),
    ("hard", 5, r"\b\d+[KkMmBb]?\s+(?:events|jobs|rows|requests|RPS|qps)\s*(?:/\s*(?:year|day|sec|s))?\b"),
    ("hard", 5, r"\b(?:replace|rewrite|untangle)\s+(?:our|the|them)?\s*(?:background\s+job|job\s+system|queue|auth|infrastructure|stack)?"),
    ("hard", 5, r"\bbundle\s+is\s+\d+(?:\.\d+)?\s?[KkMm]B\s+(?:gzipped|min\w*)"),
    ("hard", 4, r"\bcrud\s+(?:vs|or)\b|\bevent\s+sourcing\s+(?:vs|or)\b"),
    ("hard", 4, r"\bcdn\s*/\s*origin\b"),
    ("hard", 4, r"\bdata\s+residency\b"),
    ("hard", 4, r"\bnetwork\s+blip\b"),
    ("hard", 4, r"\bgrpc\s+(?:for|vs)\b|\bkafka\b"),
    ("hard", 4, r"\bcompliance\s+req"),
    ("hard", 4, r"\bgradually\s+drift"),
    ("hard", 5, r"\bgoing\s+forward\b.*?(?:sdk|drift|sync)"),
    ("hard", 4, r"\bevery\s+pii\b"),
]

# Pre-compiled view of RULES, built ONCE at import time. `classify()` runs on
# every operator prompt (the hottest per-prompt path in the harness), so the
# ~150 per-rule regex evaluations must not re-resolve the (pattern, flags)
# pair through `re`'s internal cache on every call. Compiling up front and
# iterating the pattern objects directly is behaviour-identical to
# `re.finditer(pat, text, re.IGNORECASE)` but skips the per-call cache lookup.
# RULES stays the readable source of truth; this is its compiled mirror.
COMPILED_RULES = [(tier, weight, re.compile(pat, re.IGNORECASE)) for tier, weight, pat in RULES]

# Bulk indicators — if present alongside a cheap-verb match, boost CHEAP.
BULK_INDICATORS = re.compile(
    r"\b(?:every|all|each|whole|entire|across\s+(?:our\s+)?(?:the\s+)?"
    r"(?:codebase|repo|monorepo|packages?|test\s+suite))\b",
    re.IGNORECASE,
)

# Action verbs that mark a mass-operation when bulk indicators are present.
MASS_OP_VERBS = re.compile(
    r"\b(?:add|find|update|change|fix|drop|strip|swap|remove|move|replace|"
    r"rewrite|wrap(?:ped)?|standardi[sz]e|preserve|set|insert)\b",
    re.IGNORECASE,
)

# Bounded scope markers — suppress HARD signals, lift MAIN.
BOUNDED_INDICATORS = re.compile(
    r"\b("
    r"single\s+(?:test|file|component|hook|endpoint|query|class|function|variable|form|field)"
    r"|just\s+(?:this|one)\s+(?:file|test|component|hook|endpoint|class|variable|function|module)"
    r"|this\s+specific"
    r"|this\s+one\s+file"
    r"|in\s+`[^`]+`"
    r"|in\s+\S+\.[a-z]{2,5}\b"
    r"|single\s+(?:react\s+)?hook"
    r"|one\s+(?:hook|file|component|query|function|module)"
    r"|small[-\s]d|\(small\s*d?\)|small\s+d\b|design\s+\(small\)"
    r"|two\s+(?:components|services)\b"
    r"|\bone\s+endpoint\b"
    r"|single\s+component"
    r"|in\s+a\s+single\s+file"
    r"|one\s+variable"
    r"|design\s+a\s+small\b"
    r"|propose\s+(?:a\s+)?better\s+name\b"
    r")",
    re.IGNORECASE,
)

# Negation patterns that, when appearing within ~80 chars before a HARD
# keyword, cancel that HARD signal.
NEGATION = re.compile(
    r"(?:don'?t|do\s+not|no\s+(?:design|architecture|need\s+to|logic|behavioural?|behavioral)"
    r"|avoid|skip|it'?s\s+tempting\s+to|we'?re\s+not"
    r"|i\s+know\s+this\s+looks\s+like"
    r"|but\s+(?:it'?s\s+not|really)\b"
    r"|no\s+\S+\s+(?:decisions?|changes?)"
    r"|already\s+designed)",
    re.IGNORECASE,
)


def collect_matches(prompt):
    matches = []  # (tier, weight, regex, span_text, start_idx)
    for tier, weight, regex in COMPILED_RULES:
        for m in regex.finditer(prompt):
            matches.append((tier, weight, regex, m.group(0), m.start()))
    return matches


def apply_negation(prompt, matches):
    """Cancel HARD matches whose preceding 80 chars contain a negation."""
    suppressed = []
    for tier, weight, regex, span, start in matches:
        if tier != "hard":
            suppressed.append((tier, weight, regex, span, start, False))
            continue
        window = prompt[max(0, start - 100):start]
        if NEGATION.search(window):
            suppressed.append((tier, weight, regex, span, start, True))
        else:
            suppressed.append((tier, weight, regex, span, start, False))
    return suppressed


def classify(prompt):
    scores = {"cheap": 0, "main": 0, "hard": 0}
    triggered = []

    matches = collect_matches(prompt)
    annotated = apply_negation(prompt, matches)

    for tier, weight, regex, span, start, negated in annotated:
        if negated:
            triggered.append({"tier": tier, "weight": 0, "span": span, "neg": True})
            # Negated HARD → strong cheap signal, since prompt is *correcting*
            # the impression that this is architectural.
            scores["cheap"] += 3
            continue
        scores[tier] += weight
        triggered.append({"tier": tier, "weight": weight, "span": span, "neg": False})

    # Bulk-indicator boost: if any cheap-verb match exists AND bulk indicators
    # are present, add a structural +4 to CHEAP. Helps "Across the whole
    # codebase, replace X with Y" beat HARD trap "across".
    has_cheap = any(t["tier"] == "cheap" and not t["neg"] for t in triggered)
    if has_cheap and BULK_INDICATORS.search(prompt):
        scores["cheap"] += 4
        triggered.append({"tier": "cheap", "weight": 4, "span": "[bulk-boost]", "neg": False})

    # Mass-operation meta-rule: when bulk indicators ("every|all|each") +
    # action verb (add|find|update|change|fix|...) coexist AND no HARD signal
    # is present, lift CHEAP by +4. Catches "every <thing> should ..." prose
    # without a named verb-noun pair in the rule list.
    if (
        scores["hard"] == 0
        and BULK_INDICATORS.search(prompt)
        and MASS_OP_VERBS.search(prompt)
    ):
        scores["cheap"] += 4
        triggered.append({"tier": "cheap", "weight": 4, "span": "[mass-op]", "neg": False})

    # Bounded-scope suppression: if HARD signals exist but bounded markers
    # are present, subtract from HARD and add to MAIN. Helps "Investigate
    # this specific test (file:line)" become MAIN, not HARD.
    if scores["hard"] > 0 and BOUNDED_INDICATORS.search(prompt):
        scores["hard"] = max(0, scores["hard"] - 6)
        scores["main"] += 4
        triggered.append({"tier": "main", "weight": 4, "span": "[bounded-scope]", "neg": False})

    # Decision: highest score wins; tie → HARD (escalation-biased).
    # All-zero → MAIN default.
    if scores["cheap"] == 0 and scores["hard"] == 0:
        return "main", scores, triggered

    if scores["hard"] > scores["cheap"] and scores["hard"] > scores["main"]:
        return "hard", scores, triggered
    if scores["cheap"] > scores["hard"] and scores["cheap"] > scores["main"]:
        return "cheap", scores, triggered
    if scores["main"] > scores["cheap"] and scores["main"] > scores["hard"]:
        return "main", scores, triggered

    # Tie path
    if scores["hard"] == max(scores.values()):
        return "hard", scores, triggered
    if scores["cheap"] == max(scores.values()):
        return "cheap", scores, triggered
    return "main", scores, triggered


TIER_REMINDERS = {
    "cheap": (
        "═════════════════════════════════════════════════════════════\n"
        "MODEL SWAP RECOMMENDED: opus → haiku   (≈ 15× cost reduction)\n"
        "═════════════════════════════════════════════════════════════\n\n"
        "**Dispatch directive:** Do NOT execute this turn on the current model. "
        "Invoke the Agent tool with `subagent_type: \"100x-trooper\"` "
        "(pins `model: haiku`).\n\n"
        "**Write a THICK, self-contained dispatch prompt.** The sub-agent starts "
        "cold with no access to this conversation. Include inline:\n"
        "- Exact file path(s) to operate on\n"
        "- Exact symbol / pattern to find and what to replace it with\n"
        "- Any code snippet or context the sub-agent needs to act correctly\n"
        "Do NOT write 'see conversation above' or pass the raw user message verbatim "
        "if it lacks file paths or context — synthesise a precise, standalone task.\n\n"
        "Why: classifier sees a mechanical / deterministic transform — "
        "Haiku 4.5 is ~90% as good as Sonnet here at roughly 1/15 the cost. "
        "If the cheap-tier executor finds the task is actually ambiguous, it "
        "will emit `[100x-trooper] escalate: <reason>` — re-dispatch then to "
        "`100x-specialist` or `100x-officer`."
    ),
    "main": (
        "═════════════════════════════════════════════════════════════\n"
        "MODEL SWAP RECOMMENDED: opus → sonnet  (≈ 5× cost reduction)\n"
        "═════════════════════════════════════════════════════════════\n\n"
        "**Dispatch directive:** Bounded feature/bug-fix work. Invoke the "
        "Agent tool with `subagent_type: \"100x-specialist\"` (pins `model: sonnet`).\n\n"
        "**Write a focused task description with key pointers.** The sub-agent "
        "inherits project conventions from the system prompt — you don't need to "
        "re-explain the codebase. Include only what the system prompt cannot supply:\n"
        "- Exact file path(s) and the specific function / line to change\n"
        "- The goal in one sentence and any non-obvious constraint\n"
        "- Any decision already made in this conversation that affects the task\n"
        "Do NOT paste entire files or conversation summaries — precision beats volume.\n\n"
        "If the work spawns clearly mechanical subtasks (renames, bulk type "
        "annotations, formatting), dispatch THOSE to `100x-trooper`. Escalate "
        "to `100x-officer` only if architectural scope reveals itself."
    ),
    "hard": (
        "═════════════════════════════════════════════════════════════\n"
        "MODEL ROUTING: stay on opus           (architectural work)\n"
        "═════════════════════════════════════════════════════════════\n\n"
        "**Dispatch directive:** Architecture / migration / cross-service / "
        "deep investigation. Invoke `100x-officer` (pins `model: opus`) so the "
        "transcript marker `[100x-officer @ opus]` makes the routing decision "
        "auditable.\n\n"
        "**Write a precise architectural question with key pointers.** The "
        "sub-agent inherits project conventions from the system prompt. Add only "
        "what the system prompt cannot supply:\n"
        "- The specific decision or design question to answer\n"
        "- Constraints and rejected alternatives established in this conversation\n"
        "- Repo paths for the 2–3 most relevant files (don't narrate the whole tree)\n"
        "Compression risk: summarising complex context loses nuance. Prefer sharp "
        "pointers over prose summaries — the sub-agent can read the files it needs.\n\n"
        "Decompose aggressively: mechanical subtasks → `100x-trooper`, bounded "
        "implementation subtasks → `100x-specialist`. "
        "Spend Opus tokens on reasoning, not editing."
    ),
}

DOMAIN_FLAGS = {
    "auth": r"\b(?:auth|authn|authz|login|session|jwt|oauth|sso|password|credential)\b",
    "payment": r"\b(?:payment|billing|invoice|charge|refund|stripe|checkout)\b",
    "migration": r"\b(?:migration|migrate|schema\s+change|backfill)\b",
    "security": r"\b(?:security|vulnerability|cve|xss|sqli|injection|exploit|secret\s+leak)\b",
}

# Pre-compiled once at import time — same per-prompt-path rationale as
# COMPILED_RULES (these fire on every prompt in `main()`).
COMPILED_DOMAIN_FLAGS = {name: re.compile(pat, re.IGNORECASE) for name, pat in DOMAIN_FLAGS.items()}


def emit_log(record):
    try:
        LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        with LOG_PATH.open("a", encoding="utf-8") as fh:
            fh.write(json.dumps(record, ensure_ascii=False) + "\n")
    except Exception:
        pass


def main():
    try:
        payload = json.load(sys.stdin)
    except Exception:
        return 0

    prompt = (payload.get("prompt") or "").strip()
    if not prompt:
        return 0

    tier, scores, triggered = classify(prompt)
    flags = [
        name for name, pat in COMPILED_DOMAIN_FLAGS.items()
        if pat.search(prompt)
    ]
    word_count = len(prompt.split())

    record = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "session_id": payload.get("session_id"),
        "cwd": payload.get("cwd"),
        "tier": tier,
        "scores": scores,
        "triggered": [{"t": t["tier"], "w": t["weight"], "s": t["span"], "n": t["neg"]} for t in triggered],
        "word_count": word_count,
        "domain_flags": flags,
        "prompt_head": prompt[:160],
    }
    emit_log(record)

    body = TIER_REMINDERS[tier]
    score_str = f"cheap={scores['cheap']},main={scores['main']},hard={scores['hard']}"
    header = (
        f"[model-router] tier={tier} scores=({score_str}) "
        f"words={word_count} domains={flags or 'none'}"
    )

    output = {
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": f"{header}\n\n{body}",
        }
    }
    sys.stdout.write(json.dumps(output))
    return 0


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