# ==============================================================================
# PORTED FROM STARSEND: lib/prompt_config.py
# DATE: 2026-03-29
# NOTE: For historical git blame prior to this date, see the starsend repository.
# ==============================================================================
"""
prompt_config.py — Loader for prompt_constants.json and lexicon.json.

Single source of truth for all production and pre-production guard texts,
kinetic descriptors, and lighting maps. Supports project-level overrides.

Usage:
    from recoil.core.prompt_config import get_constant, get_kinetic_descriptor

    camera = get_constant("production", "camera_body")
    kinetic = get_kinetic_descriptor("She pushes the door open")
"""

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

from recoil.core.paths import CONFIG_DIR
from recoil.core.exceptions import ConfigParseError

logger = logging.getLogger(__name__)

_CONSTANTS_PATH = CONFIG_DIR / "prompt_constants.json"
_LEXICON_PATH = CONFIG_DIR / "lexicon.json"

# Cached after first load
_constants: Optional[dict] = None
_lexicon: Optional[dict] = None
_compiled_kinetic: Optional[list] = None
_compiled_light_dir: Optional[list] = None
_compiled_light_qual: Optional[list] = None


def load_prompt_file(filename: str) -> str:
    """Load a versioned prompt file from config/prompts/.

    Args:
        filename: Name of the prompt file (e.g. "flash_to_nbp_v1.0.txt").

    Returns:
        Stripped file contents, or empty string if the file is missing.
    """
    path = CONFIG_DIR / "prompts" / filename
    if path.is_file():
        return path.read_text(encoding="utf-8").strip()
    logger.warning("Prompt file not found: %s", path)
    return ""


def load_constants() -> dict:
    """Load prompt constants from config file (cached)."""
    global _constants
    if _constants is None:
        from recoil.core.config_schema import validate_and_load
        _constants = validate_and_load(_CONSTANTS_PATH, "prompt_constants")
    return _constants


def get_constant(
    category: str,
    key: str,
    default: str = "",
    project_dir: Optional[str] = None,
) -> str:
    """Get a prompt constant by category and key.

    Args:
        category: "production", "casting", or "shared"
        key: The constant key (e.g. "camera_body")
        default: Fallback if key not found
        project_dir: Optional project directory for per-project overrides
    """
    # Check project-level override first
    # TENET_6_DEFERRED_TO_PHASE_E: per-project prompt_constants.json silent
    # fallback (intentional per data-contracts §override-loaders).
    if project_dir:
        override_path = Path(project_dir) / "prompt_constants.json"
        if override_path.exists():
            try:
                overrides = json.loads(override_path.read_text(encoding="utf-8"))
                if category in overrides and key in overrides[category]:
                    return overrides[category][key]
            except FileNotFoundError:
                pass  # missing config is OK
            except (json.JSONDecodeError, OSError) as e:
                logger.warning(
                    "prompt_config: corrupt at %s — %s", override_path, e,
                )
                raise ConfigParseError(
                    str(override_path), message=str(e)
                ) from e

    constants = load_constants()
    return constants.get(category, {}).get(key, default)


def load_lexicon() -> dict:
    """Load lexicon from config file (cached)."""
    global _lexicon
    if _lexicon is None:
        from recoil.core.config_schema import validate_and_load
        _lexicon = validate_and_load(_LEXICON_PATH, "lexicon")
    return _lexicon


def _compile_kinetic() -> list:
    """Compile kinetic regex patterns (cached)."""
    global _compiled_kinetic
    if _compiled_kinetic is None:
        lex = load_lexicon()
        _compiled_kinetic = [
            (re.compile(entry["pattern"], re.I), entry["descriptor"])
            for entry in lex["kinetic_map"]
        ]
    return _compiled_kinetic


def get_kinetic_descriptor(text: str) -> str:
    """Match text against kinetic lexicon, return first matching descriptor."""
    for pattern, descriptor in _compile_kinetic():
        if pattern.search(text):
            return descriptor
    return load_lexicon().get("fallback", "neutral motion")


def get_all_kinetic_descriptors(text: str, max_count: int = 3) -> list:
    """Match text against kinetic lexicon, return up to *max_count* descriptors.

    Uses the cached compiled kinetic patterns internally. Falls back to the
    shared kinetic_fallback constant when nothing matches.

    Args:
        text: Free-text to match against kinetic patterns.
        max_count: Maximum number of descriptors to return (default 3).

    Returns:
        List of matching descriptor strings (deduplicated, order-preserving).
    """
    matched: list[str] = []
    for pattern, descriptor in _compile_kinetic():
        if pattern.search(text):
            if descriptor not in matched:
                matched.append(descriptor)
                if len(matched) >= max_count:
                    break
    if not matched:
        fallback = get_constant("shared", "kinetic_fallback")
        return [fallback] if fallback else [load_lexicon().get("fallback", "neutral motion")]
    return matched


def _compile_light_dir() -> list:
    """Compile lighting direction patterns (cached)."""
    global _compiled_light_dir
    if _compiled_light_dir is None:
        lex = load_lexicon()
        _compiled_light_dir = [
            (re.compile(entry["pattern"], re.I), entry["direction"])
            for entry in lex.get("lighting_direction_map", [])
        ]
    return _compiled_light_dir


def get_lighting_direction(text: str) -> Optional[str]:
    """Match text against lighting direction map."""
    for pattern, direction in _compile_light_dir():
        if pattern.search(text):
            return direction
    return None


def _compile_light_qual() -> list:
    """Compile lighting quality patterns (cached)."""
    global _compiled_light_qual
    if _compiled_light_qual is None:
        lex = load_lexicon()
        _compiled_light_qual = [
            (re.compile(entry["pattern"], re.I), entry["quality"])
            for entry in lex.get("lighting_quality_map", [])
        ]
    return _compiled_light_qual


def get_lighting_quality(text: str) -> Optional[str]:
    """Match text against lighting quality map."""
    for pattern, quality in _compile_light_qual():
        if pattern.search(text):
            return quality
    return None


def reload():
    """Force reload all config from disk."""
    global _constants, _lexicon, _compiled_kinetic, _compiled_light_dir, _compiled_light_qual
    _constants = None
    _lexicon = None
    _compiled_kinetic = None
    _compiled_light_dir = None
    _compiled_light_qual = None


__all__ = [
    # Public symbols (Phase D — MF-3 + DEBT-9).
    # Loaders.
    "load_constants",
    "load_lexicon",
    "load_prompt_file",
    # Constant accessors.
    "get_constant",
    # Kinetic accessors.
    "get_all_kinetic_descriptors",
    "get_kinetic_descriptor",
    # Lighting accessors.
    "get_lighting_direction",
    "get_lighting_quality",
    # Cache-control.
    "reload",
]
