# recoil/pipeline/lib/payload_builder.py
"""Declarative payload builder. Reads model rules from the extended profile.

Spec §C lines 1854-1894 — paste-ready sketch.
"""

from __future__ import annotations

import re
from typing import Any

from recoil.core.model_profiles import get_profile


def build_payload(*, shot, refs, prompt, model_id) -> dict[str, Any]:
    profile = get_profile(model_id)

    # 1. Apply effective ref cap for character refs
    max_char_refs_api = profile.get("max_character_refs", 10)
    effective = profile.get("effective_max_character_refs")
    max_char_refs_effective = effective if effective is not None else max_char_refs_api
    max_char_refs = min(max_char_refs_api, max_char_refs_effective)

    char_refs = [r for r in refs if r.role == "character"][:max_char_refs]
    other_refs = [r for r in refs if r.role != "character"]

    # 2. Role labeling in prompt
    if profile.get("requires_ref_role_labeling"):
        prompt = _inject_role_labels(prompt, char_refs)

    # 3. Strip SD-style weighting
    if profile.get("prompt_emphasis_syntax") == "natural_language":
        prompt = _strip_sd_weights(prompt)

    # 4. Convert negatives to positives
    if not profile.get("supports_negative_prompt", True):
        prompt = _convert_negatives_to_positives(prompt)

    # 5. Position bias
    if profile.get("position_bias_severity") == "high":
        char_refs = _ensure_hero_at_index_zero(char_refs)

    # 6. Grid filtering (only if calibration measured grid failures)
    if (rate := profile.get("grid_reference_failure_rate")) and rate > 0.2:
        char_refs = [r for r in char_refs if not _is_grid_image(r)]

    # 7. Style anchor
    if profile.get("requires_style_anchor_for_long_runs") and getattr(shot, "run_id", None):
        anchor = _get_style_anchor_for_run(shot.run_id)
        if anchor:
            other_refs.append(anchor)

    return {
        "prompt": prompt,
        "refs": char_refs + other_refs,
        "model": model_id,
        "profile": profile,
    }


_SD_WEIGHT = re.compile(r"\(([^()]+):\d+(\.\d+)?\)")


def _strip_sd_weights(prompt: str) -> str:
    """Convert (foo:1.5) -> foo."""
    return _SD_WEIGHT.sub(r"\1", prompt)


_NEGATIVE_PHRASES = re.compile(r",\s*no\s+\w+(\s+\w+){0,2}", re.IGNORECASE)


def _convert_negatives_to_positives(prompt: str) -> str:
    return _NEGATIVE_PHRASES.sub("", prompt)


def _inject_role_labels(prompt: str, char_refs: list) -> str:
    if not char_refs:
        return prompt
    labels = ", ".join(f"[character_ref_{i}]" for i in range(len(char_refs)))
    return f"Subject: {labels}. {prompt}"


def _ensure_hero_at_index_zero(refs: list) -> list:
    return refs


def _is_grid_image(ref) -> bool:
    return False  # filled in if Phase 0.5 measures grid_reference_failure_rate > 0.2


def _get_style_anchor_for_run(run_id: str, scene_key: str = ""):
    """Look up style anchor -- deprecated, injection happens in run_shot.py now."""
    return None
