"""
jit_prompt.py — Just-In-Time prompt hydrator + model formatters.

Replaces compile_all_prompts() bake-at-Stage-2 pattern with live hydration
from bible data at generation time. Eliminates stale wardrobe
and location contamination bugs.

Reference-image selection is NOT handled here — that is the job of
recoil.core.ref_resolver (the canonical ref monopoly). This module
only hydrates text skeletons from the bible.

Token format (produced by Stage 2 LLM):
  - {char_Name}       → resolved from bible character visual + wardrobe
  - {loc_LOCATION_ID} → resolved from bible location description

Emotion_line is left as natural language (no tokens needed).
"""

import re

# Reuse the existing character description builder from prompt_engine

# Token patterns
_CHAR_TOKEN = re.compile(r"\{char_(\w+)\}")
_LOC_TOKEN = re.compile(r"\{loc_(\w+)\}")


# ══════════════════════════════════════════════════════════════════════
# JIT HYDRATOR
# ══════════════════════════════════════════════════════════════════════


def hydrate_skeleton(
    prompt_skeleton: dict,
    bible: dict,
    episode: int = 1,
    asset_data: dict | None = None,
) -> dict:
    """Replace {char_*} and {loc_*} tokens with live bible data.

    Args:
        prompt_skeleton: Dict with subject_line, environment_line,
            action_line, emotion_line.
        bible: Global bible dict (characters, locations).
        episode: Episode number for wardrobe phase resolution.
        asset_data: Shot asset_data for character list + location_id.

    Returns:
        Hydrated skeleton dict with tokens resolved to descriptions.

    Note:
        This function previously accepted an optional ``casting_state`` dict,
        but it was never read (refs are resolved separately through
        recoil.core.ref_resolver). Parameter removed in Phase 19/20 cleanup.
    """
    result = dict(prompt_skeleton)  # shallow copy
    bible_chars = bible.get("characters", {})
    bible_locs = bible.get("locations", {})

    # ── Resolve character tokens ──────────────────────────────────
    def _resolve_char(match):
        char_id = match.group(1)
        bible_char = bible_chars.get(char_id, {})
        if not bible_char:
            return char_id  # Fallback: just the name

        display_name = bible_char.get("display_name", char_id)
        visual = bible_char.get("visual_description", "")

        # Resolve wardrobe from episode-appropriate phase
        wardrobe = ""
        phases = bible_char.get("phases", [])
        if phases:
            # Try to find matching phase from asset_data
            phase_id = ""
            if asset_data:
                for c in asset_data.get("characters", []):
                    if c.get("char_id") == char_id:
                        phase_id = c.get("wardrobe_phase_id", "")
                        break

            for phase in phases:
                if phase_id and phase.get("phase_id") == phase_id:
                    wardrobe = phase.get("wardrobe_description", "")
                    break
                # Fallback: resolve by episode range
                start = phase.get("start_ep", 0)
                end = phase.get("end_ep", 9999)
                if start <= episode <= end:
                    wardrobe = phase.get("wardrobe_description", "")
                    break

            if not wardrobe and phases:
                wardrobe = phases[0].get("wardrobe_description", "")

        desc = display_name
        if visual:
            desc += f" ({visual}"
            if wardrobe:
                desc += f", wearing {wardrobe}"
            desc += ")"
        elif wardrobe:
            desc += f" (wearing {wardrobe})"

        return desc

    # ── Resolve location tokens ───────────────────────────────────
    def _resolve_loc(match):
        loc_id = match.group(1)
        bible_loc = bible_locs.get(loc_id, {})
        if not bible_loc:
            # Try case-insensitive lookup
            for key, val in bible_locs.items():
                if key.upper() == loc_id.upper():
                    bible_loc = val
                    break

        if not bible_loc:
            return loc_id.replace("_", " ")

        desc = bible_loc.get("visual_description", "")
        if not desc:
            desc = bible_loc.get("description", loc_id.replace("_", " "))
        return desc

    # Apply token resolution to subject_line and environment_line
    for field in ("subject_line", "environment_line", "action_line"):
        text = result.get(field, "")
        if text:
            text = _CHAR_TOKEN.sub(_resolve_char, text)
            text = _LOC_TOKEN.sub(_resolve_loc, text)
            result[field] = text

    return result


def hydrate_shot(shot: dict, bible: dict, episode: int = 1) -> dict:
    """Hydrate a full shot's prompt_skeleton in-place (mutates shot copy).

    Convenience wrapper — reads skeleton from shot's prompt_data,
    hydrates, and writes back. Returns the shot dict (mutated).
    """
    prompt_data = shot.get("prompt_data", {})
    skeleton = prompt_data.get("prompt_skeleton", {})
    if not skeleton:
        return shot

    asset_data = shot.get("asset_data", {})
    hydrated = hydrate_skeleton(skeleton, bible, episode=episode, asset_data=asset_data)
    prompt_data["prompt_skeleton"] = hydrated
    shot["prompt_data"] = prompt_data
    return shot


# ══════════════════════════════════════════════════════════════════════
# STERILITY GATE — validates Stage 2 output
# ══════════════════════════════════════════════════════════════════════


def validate_sterility(shot: dict, bible: dict) -> list[str]:
    """Check environment_line for character name contamination.

    Returns list of violation strings (empty = clean).
    """
    skeleton = shot.get("prompt_data", {}).get("prompt_skeleton", {})
    env_line = skeleton.get("environment_line", "")
    if not env_line:
        return []

    violations = []
    bible_chars = bible.get("characters", {})

    for char_id, char_data in bible_chars.items():
        display_name = char_data.get("display_name", char_id)
        # Check for raw character names in environment_line (should be tokens)
        if display_name and display_name.lower() in env_line.lower():
            violations.append(
                f"Character name '{display_name}' found in environment_line"
            )

    return violations


def auto_fix_tokens(shot: dict, bible: dict) -> dict:
    """Auto-fix character names to {char_*} tokens if LLM missed them.

    Operates on subject_line and environment_line.
    Returns modified shot dict.
    """
    skeleton = shot.get("prompt_data", {}).get("prompt_skeleton", {})
    bible_chars = bible.get("characters", {})

    for field in ("subject_line", "environment_line"):
        text = skeleton.get(field, "")
        if not text:
            continue

        for char_id, char_data in bible_chars.items():
            display_name = char_data.get("display_name", char_id)
            if not display_name:
                continue

            # Replace bare character names with tokens (case-insensitive)
            # But skip if already tokenized
            token = f"{{char_{char_id}}}"
            if token in text:
                continue

            pattern = re.compile(re.escape(display_name), re.IGNORECASE)
            if pattern.search(text):
                text = pattern.sub(token, text)

        skeleton[field] = text

    shot.setdefault("prompt_data", {})["prompt_skeleton"] = skeleton
    return shot


# RETIRED 2026-06-09: enhance_prompt() + _ENRICHMENT_SYSTEM_PROMPTS +
# _call_anthropic/_call_gemini (LLM prompt "enrichment") — superseded by the
# prose_author strategy path (author_strategies.py + dispatch_payload.py),
# which authors DP-voice prose at generation time. Was only reachable from
# the deprecated 8430 console and a manual API route, never the render path.
