"""
bible_loader.py — Load and query the Prompt Bible (PROMPT_BIBLE.yaml).

Provides typed accessors for model-specific prompting rules, ref limits,
aspect ratio behavior, and gotchas. Caches the parsed YAML in module state.
"""

from __future__ import annotations

from typing import Optional

from recoil.core.paths import CONFIG_DIR

_BIBLE_PATH = CONFIG_DIR / "PROMPT_BIBLE.yaml"

_bible: Optional[dict] = None


def load_bible() -> dict:
    """Load PROMPT_BIBLE.yaml, caching in module-level _bible.

    Returns model_id → rule-block dict. The top-level `schema_version`
    metadata key (required by the Pydantic schema) is stripped from the
    returned dict so callers iterating model entries never have to
    special-case it.
    """
    global _bible
    if _bible is None:
        from recoil.core.config_schema import validate_and_load
        raw = validate_and_load(_BIBLE_PATH, "prompt_bible")
        # Strip schema_version — model entries only.
        _bible = {k: v for k, v in raw.items() if k != "schema_version"}
    return _bible


def reload_bible() -> None:
    """Clear cache and reload from disk."""
    global _bible
    _bible = None
    load_bible()


def get_model_rules(model_name: str) -> dict | None:
    """Return the full model block, or None if model not found."""
    return load_bible().get(model_name)


def get_global_defaults() -> dict:
    """Return the top-level `global_defaults:` block, or {} if absent.

    Holds cross-model prompting toggles that builders read directly (not
    routed through `get_prompt_rules` because they apply across all models).
    Per-model rule blocks may shadow any key via their own value of the
    same name — see `get_prompt_rule_with_global_default`.
    """
    gd = load_bible().get("global_defaults") or {}
    return gd if isinstance(gd, dict) else {}


def get_prompt_rule_with_global_default(model_name: str, key: str, default=None):
    """Resolve a prompt-rule key with per-model override on global default.

    Lookup order:
      1. `<model>.prompt.<key>` (if model exists and has a prompt block)
      2. `global_defaults.<key>`
      3. `default` argument

    Used by `_build_camera_line_plan` to read `include_focal_length`, but
    safe for any flag that conceptually belongs to global_defaults with
    a possible per-model escape hatch.
    """
    try:
        model_rules = get_model_rules(model_name)
    except Exception:
        model_rules = None
    if model_rules is not None:
        prompt_block = model_rules.get("prompt") if isinstance(model_rules, dict) else None
        if isinstance(prompt_block, dict) and key in prompt_block:
            return prompt_block[key]
    gd = get_global_defaults()
    if key in gd:
        return gd[key]
    return default


def get_prompt_rules(model_name: str) -> dict:
    """Return the prompt section. Raises KeyError if model unknown."""
    rules = get_model_rules(model_name)
    if rules is None:
        raise KeyError(
            f"Unknown model: {model_name}. "
            f"Available: {', '.join(sorted(load_bible().keys()))}"
        )
    return rules["prompt"]


def get_ref_rules(model_name: str) -> dict:
    """Return the refs section. Raises KeyError if model unknown."""
    rules = get_model_rules(model_name)
    if rules is None:
        raise KeyError(
            f"Unknown model: {model_name}. "
            f"Available: {', '.join(sorted(load_bible().keys()))}"
        )
    return rules["refs"]


def get_ar_rules(model_name: str) -> dict:
    """Return the aspect_ratio section. Raises KeyError if model unknown."""
    rules = get_model_rules(model_name)
    if rules is None:
        raise KeyError(
            f"Unknown model: {model_name}. "
            f"Available: {', '.join(sorted(load_bible().keys()))}"
        )
    return rules["aspect_ratio"]


def get_frame_rules(model_name: str) -> dict | None:
    """Return first_last_frame section, or None for image models."""
    rules = get_model_rules(model_name)
    if rules is None:
        raise KeyError(
            f"Unknown model: {model_name}. "
            f"Available: {', '.join(sorted(load_bible().keys()))}"
        )
    flf = rules.get("first_last_frame")
    if flf is None:
        return None
    # Image models have all-null frame params — return None for those
    if flf.get("start_param") is None and flf.get("end_param") is None:
        return None
    return flf


def supports_negative_prompt(model_name: str) -> bool:
    """Quick accessor: does this model support negative prompts?"""
    prompt = get_prompt_rules(model_name)
    return bool(prompt.get("negative_prompt", False))


def get_optimal_word_range(model_name: str, mode: str = "default") -> tuple[int, int]:
    """Return (min, max) from prompt.optimal_words.

    Supports mode-specific ranges: if mode="i2v" and prompt.optimal_words.i2v
    exists, use that; otherwise fall back to prompt.optimal_words.default or
    the flat prompt.optimal_words list.
    """
    prompt = get_prompt_rules(model_name)
    ow = prompt["optimal_words"]

    # optimal_words can be a dict with mode keys or a flat list
    if isinstance(ow, dict):
        # Try requested mode first, then fall back to default
        if mode in ow:
            rng = ow[mode]
        else:
            rng = ow.get("default", list(ow.values())[0])
    else:
        # Flat list [min, max]
        rng = ow

    return (rng[0], rng[1])


def get_i2v_ar_behavior(model_name: str) -> str | None:
    """Return 'respects_param' or 'matches_start_frame' or None."""
    ar = get_ar_rules(model_name)
    return ar.get("i2v_behavior")


def get_gotchas(model_name: str) -> list[str]:
    """Return gotchas list for a model."""
    rules = get_model_rules(model_name)
    if rules is None:
        raise KeyError(
            f"Unknown model: {model_name}. "
            f"Available: {', '.join(sorted(load_bible().keys()))}"
        )
    return rules.get("gotchas", [])
