#!/usr/bin/env python3
"""
prompt_validators.py — Post-Compile Validation for Prompt Compiler

VerbStrengthValidator: checks if Layer 3 (action_pose) has emotional payload.
Runs AFTER compile(), not during — clean separation.

Usage:
    from prompt_validators import validate_verb_strength

    result = compile(shot, breakdown, episode, ...)
    warning = validate_verb_strength(result, shot, frame_type)
    if warning:
        print(f"Weak verb: {warning['suggestion']}")
"""

import json
import logging
import re
from pathlib import Path
from typing import Optional

from recoil.pipeline._lib.sanctioned_fallbacks import (
    FallbackRecord,
    fire_sanctioned_fallback,
    register_sanctioned_fallback,
)


# Tenet 6: corrupt verb_patterns.json overrides falling back to the documented
# built-in patterns is named + observable per the registry.
register_sanctioned_fallback(
    FallbackRecord(
        name="prompt_validator_pattern_default_builtins",
        justification=(
            "verb_patterns.json overrides file fails to parse (corrupt JSON, "
            "permission error). The validator falls back to the documented "
            "built-in pattern lists (_BUILTIN_MICRO_DETAIL_PATTERNS, "
            "_BUILTIN_BARE_PATTERNS) — same as if the override file did not "
            "exist."
        ),
        quality_neutrality_argument=(
            "The substitution flows ONLY into the prompt validator's static "
            "regex compilation. Validator results affect prompt-author "
            "feedback but never modify generation bytes — the prompt is "
            "validated, not rewritten. Built-in patterns ARE the documented "
            "default; loading them on parse failure is equivalent to running "
            "with no override file."
        ),
        expected_substitution=(
            "(_BUILTIN_MICRO_DETAIL_PATTERNS, _BUILTIN_BARE_PATTERNS) tuple "
            "in place of the parsed verb_patterns.json values"
        ),
        introduced_in="Phase E.debug-R3",
    )
)

log = logging.getLogger(__name__)


# ── Frame Types → Verb Classes ───────────────────────────────────────────

VERB_CLASSES = {
    "hero": "ACTIVE",
    "first": "PREPARATION",
    "last": "RESULT",
}

# ── Pattern Loading ─────────────────────────────────────────────────────

_GAP = r"(?:\s+\S+){0,5}\s+"

# Built-in patterns (fallback if verb_patterns.json not found)
_BUILTIN_MICRO_DETAIL_PATTERNS = [
    # Body micro-details
    rf"jaw{_GAP}(tight|clench|set|grind|lock|press|work)",
    rf"fingers?{_GAP}(curl|grip|white|clench|spread|dig|find|trace|press|wrap|drum|tap|trail|wedge|wrench|reach|extend|yank)",
    rf"knuckle[s]?{_GAP}(white|whiten|tight|grip|crack|press|span|encrust)",
    rf"teeth{_GAP}(bare|grit|clench|show|grind|set)",
    rf"eyes?{_GAP}(wid|narrow|lock|track|blaz|squint|dart|scan|dilat|snap|shift|reflect|open|focus)",
    rf"iris(es)?{_GAP}(blaz|glow|dilat|contract|shift|burn|flare)",
    rf"pupil[s]?{_GAP}(dilat|contract|track|wid)",
    rf"gaze{_GAP}(lock|shift|cast|fix|track|assess|sweep|narrow)",
    rf"brow{_GAP}(furrow|raise|knit|crease|heavy)",
    rf"lips?{_GAP}(press|part|crack|thin|curl|tighten|split)",
    rf"mouth{_GAP}(open|press|set|work|crack|tighten)",
    rf"shoulder[s]?{_GAP}(square|brace|hunch|drop|tense|set|pull)",
    rf"chest{_GAP}(ris|heav|expand|compress|tight)",
    rf"breath{_GAP}(held|catch|ragg|quick|shallow|visible|fog)",
    rf"breathing{_GAP}(hard|heavy|ragg|shallow|fast)",
    rf"muscle[s]?{_GAP}(tens|strain|flex|bulg|taut|lock|fire)",
    rf"tendon[s]?{_GAP}(strain|vis|pull|tight|taut)",
    rf"vein[s]?{_GAP}(bulg|vis|pulse|throb|stand)",
    rf"sweat{_GAP}(bead|drip|glisten|sheen|slick)",
    rf"fist[s]?{_GAP}(clench|tight|white|ball|curl)",
    rf"arm[s]?{_GAP}(trem|strain|lock|brace|extend|reach|shake|lunge)",
    rf"leg[s]?{_GAP}(brace|plant|spread|drive|strain|bent|kick|swing|dangle)",
    rf"boot[s]?{_GAP}(plant|slam|dig|press|flat|slide|grip|swing|hit|clear|dangle|hover|leave|aim|land)",
    rf"wrist{_GAP}(glow|amber|compress|strain|turn|twist|press|catch|buckle)",
    rf"skin{_GAP}(flush|pale|tight|pore|goose|sweat|glow|catch|show|bloodless|marble|tighten)",
    rf"hair{_GAP}(whip|catch|slick|fall|mat|cling|crop|frame|close)",
    rf"hand[s]?{_GAP}(trem|shake|grip|clench|reach|press|steady|close|twitch|extend|lift)",
    rf"palm{_GAP}(press|flat|slick|open|close|slam|push)",
    rf"chin{_GAP}(tilt|lift|drop|set|jut|lower|raise)",
    rf"neck{_GAP}(strain|tend|tilt|crane|twist)",
    rf"body{_GAP}(torque|brace|compress|shift|lean|angle|coil|hang|suspend|lift)",
    rf"weight{_GAP}(shift|plant|forward|back|lean|drop|throw|commit)",
    rf"spine{_GAP}(straight|curve|stiffen|arch|rigid)",
    rf"face{_GAP}(flush|pale|set|shift|fill|catch|lit|illuminat|carv|strain)",
    rf"expression{_GAP}(shift|set|war|harden|soften|neutral|stun|falter)",
    rf"posture{_GAP}(emerg|shift|set|square|sag|stiffen|straighten)",
    rf"grin{_GAP}(split|spread|commit|falter|flash|widen|set)",
    rf"eyelash(es)?{_GAP}(shatter|frost|crust|catch|cling|frozen|break)",
    rf"toe[s]?{_GAP}(catch|point|clear|hover|curl|grip|dig)",
    rf"sole[s]?{_GAP}(separ|hover|clear|worn|flat|press|land)",
    rf"knee[s]?{_GAP}(compress|bend|buckle|lock|drive|bend|flex)",
    # Compound adjective forms
    r"frost[- ]whiten",
    r"sweat[- ]blacken",
    r"white[- ]knuckle",
    r"cryo[- ]pallor",
    r"scar(red)?\s+(fist|knuckle|skin|hand)",
    # Object interactions
    rf"rust{_GAP}(cascad|flak|crumbl|dust|peel|scat|fall|drift|curl|land|streak|catch|melt)",
    rf"spark[s]?{_GAP}(erupt|fly|shower|burst|scatter|arc|die|catch|ignit|trail)",
    rf"frost{_GAP}(crack|shatter|melt|cryst|form|cling|break|radi|residu|encrust|catch|refract|fragment)",
    rf"metal{_GAP}(groan|scream|creak|screech|bend|buckle|yield|gleam|catch)",
    rf"grating{_GAP}(vibrat|rattle|clang|ring|shake|corrode)",
    rf"counter{_GAP}(tick|glow|pulse|climb|flash|blaze|cast|bleed|radiat)",
    rf"gas{_GAP}(erupt|vent|billow|blast|hiss|cloud|rush|clear|dissipat|burn)",
    rf"condensation{_GAP}(bead|drip|form|fog|cloud|mist|weep)",
    rf"light{_GAP}(catch|refract|scatter|pool|wash|cast|die|flick|carv|illuminat|plung|punch|intensif)",
    rf"shadow[s]?{_GAP}(carv|pool|deep|shift|fall|stretch)",
    rf"debris{_GAP}(suspend|float|scatter|fall|drift|hang|tumbl)",
    rf"particl[e]?{_GAP}(hang|drift|float|suspend|swirl|ignit|glow|catch)",
    rf"vapor{_GAP}(explod|billow|dissipat|clear|hang|drift|burn)",
    rf"digit[s]?{_GAP}(tick|cast|climb|glow|pulse|flash)",
    rf"beam{_GAP}(plung|punch|whip|carv|cut|catch|sweep|angle|point|die|swing)",
    rf"glow{_GAP}(cast|wash|climb|pulse|radiat|bleed|intensif|warm|amber|steady)",
    # Environmental micro-details
    r"flakes?\s+drift",
    r"drip(ping|s)?\s+from",
    r"hum(ming|s)?\s+",
    r"vibrat(ing|e|es)\s+through",
    r"catching\s+(amber|light|glow|fire)",
    r"hanging\s+motionless",
    r"pulsing\s+(steady|with|blue|amber|warm|faint)",
    r"refracting\s+",
    r"gleam(ing)?\s+impossible",
    r"weeping\s+condensation",
    r"corroded\s+(wall|ladder|grating|metal|surface|catwalk|steel)",
    r"amber\s+(emergency|glow|light|LED|counter|readout)",
    r"blue-white\s+(glow|light|pulse|machinery|systems|pod|internal)",
    r"dissipating\s+(cryogenic|gas|vapor|mist|fog|smoke)",
    r"outline\s+of\s+something\s+(massive|enormous|huge|vast)",
    r"shrouded\s+in\s+(dissipating|cryogenic|gas|vapor|mist|fog)",
]

_BUILTIN_BARE_PATTERNS = [
    r"^(standing|sitting|looking|holding|walking|waiting|watching|staring|leaning) (in|at|on|by|near|toward|against|into) ",
    r"^(a|the) (woman|man|figure|person|character|salvager) (stands|sits|looks|holds|walks|waits|watches|stares|leans) ",
]

# Cache loaded patterns
_loaded_patterns = None


def _load_patterns() -> tuple:
    """Load patterns from verb_patterns.json if available, else use built-in."""
    global _loaded_patterns
    if _loaded_patterns is not None:
        return _loaded_patterns

    json_path = Path(__file__).parent / "verb_patterns.json"
    if json_path.exists():
        try:
            with open(json_path) as f:
                data = json.load(f)
            micro = data.get("micro_detail_patterns",
                             _BUILTIN_MICRO_DETAIL_PATTERNS)
            bare = data.get("bare_patterns", _BUILTIN_BARE_PATTERNS)
            _loaded_patterns = (micro, bare)
            return _loaded_patterns
        except (json.JSONDecodeError, OSError) as e:
            fire_sanctioned_fallback(
                "prompt_validator_pattern_default_builtins",
                json_path=str(json_path),
                error=str(e),
            )

    _loaded_patterns = (_BUILTIN_MICRO_DETAIL_PATTERNS,
                        _BUILTIN_BARE_PATTERNS)
    return _loaded_patterns


# ── Validator ───────────────────────────────────────────────────────────

def validate_verb_strength(
    compiled_result: dict,
    shot: dict,
    frame_type: str,
) -> Optional[dict]:
    """Check if Layer 3 (action_pose) has emotional payload.

    Called AFTER compile(). Returns warning dict if weak, None if strong.

    Args:
        compiled_result: Return value from compile().
        shot: Original shot dict.
        frame_type: "hero", "first", or "last".

    Returns:
        None if strong, or dict with:
            shot_id, frame_type, current, detail_count, suggestion
    """
    action = compiled_result.get("layers", {}).get("action_pose", "")
    if not action:
        return {
            "shot_id": shot.get("id", 0),
            "frame_type": frame_type,
            "current": "(empty)",
            "detail_count": 0,
            "suggestion": "Add action description with 2+ physical micro-details",
        }

    action_lower = action.lower()
    micro_patterns, bare_patterns = _load_patterns()

    # Count physical micro-details
    detail_count = 0
    for pattern in micro_patterns:
        if re.search(pattern, action_lower):
            detail_count += 1

    # Check for bare verb + noun (weak)
    is_bare = False
    for pattern in bare_patterns:
        if re.match(pattern, action_lower):
            is_bare = True
            break

    # Need at least 2 micro-details for hero, 1 for first/last
    min_details = 2 if frame_type == "hero" else 1

    if detail_count < min_details or (is_bare and detail_count < 2):
        return {
            "shot_id": shot.get("id", 0),
            "frame_type": frame_type,
            "current": action[:120],
            "detail_count": detail_count,
            "suggestion": _suggest_fix(action, frame_type),
        }

    return None


def _suggest_fix(action: str, frame_type: str) -> str:
    """Generate a suggestion for strengthening weak verb prompts."""
    verb_class = VERB_CLASSES.get(frame_type, "ACTIVE")

    if verb_class == "ACTIVE":
        return (
            "Add 2+ physical micro-details to hero action. "
            "E.g.: body language (jaw tightening, knuckles whitening), "
            "object interaction (rust cascading, sparks erupting), "
            "or environmental response (grating vibrating, condensation dripping)"
        )
    elif verb_class == "PREPARATION":
        return (
            "Add anticipation details: fingers finding grip, "
            "weight shifting, eyes tracking, breath held"
        )
    else:
        return (
            "Add aftermath details: breathing hard, "
            "body settling, debris still falling, grip loosening"
        )
