"""T2I 프롬프트 안전 정책 수정 모듈 — 거절된 프롬프트를 LLM 클라이언트로 수정.

prompt_loader 통합 (problems.md #14): 기존 hard-coded ``v1`` 디렉토리 직접 read
제거 → ``prompt_loader.load_prompt`` 경유하여 DB prompt_template 우선 + numeric-
aware version 정렬 + version pack drift detection (#6) + local jsonschema
validation (#13) 의 통합 정책을 적용 받는다.
"""

import logging
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

_MODULE = "prompt_sanitizer"

SANITIZE_SCHEMA: Dict[str, Any] = {
    "type": "object",
    "properties": {
        "sanitized_prompt": {"type": "string"},
        "changes": {"type": "string"},
        "strategy": {"type": "string"},
    },
    "required": ["sanitized_prompt", "changes", "strategy"],
    "additionalProperties": False,
}

STRATEGIES: Dict[str, Dict[str, str]] = {
    "film_previs": {
        "name": "영화 프리비즈",
        "description": "촬영 전 컨셉아트 형태로 재구성",
        "prefix": (
            "This is a pre-visualization concept art for a film production. "
            "Storyboard-quality still frame for the director's shot planning. "
            "Cinematic composition, dramatic lighting, professional film production context. "
            "NO gore, NO explicit violence, NO blood — focus on dramatic tension and emotion.\n\n"
        ),
    },
    "movie_poster": {
        "name": "영화 포스터",
        "description": "감정과 분위기 중심의 키 비주얼로 재구성",
        "prefix": (
            "A cinematic movie poster key visual for a feature film. "
            "Focus on the emotional core — characters' faces, dramatic poses, atmospheric lighting. "
            "Professional film marketing quality. Show mood and tension, not violence.\n\n"
        ),
    },
    "aftermath": {
        "name": "직후 정적 장면",
        "description": "액션 직후의 정적인 순간으로 시점 변경",
        "prefix": (
            "The moment AFTER the action — a quiet, contemplative still frame. "
            "Characters process what just happened. Focus on facial expressions, "
            "body posture, environmental details. Cinematic, thoughtful, no active violence.\n\n"
        ),
    },
}

ATTEMPT_TO_STRATEGY = {
    1: "film_previs",
    2: "movie_poster",
    3: "aftermath",
}


_SEMANTIC_OVERRIDE_MARKER = "SEMANTIC OVERRIDE:"

_STATE_GUIDANCE: Dict[str, str] = {
    "dead":             "Do not rewrite as alive, unharmed, asleep, or emotionally recovering.",
    "unconscious":      "Do not rewrite as conscious, reacting, emotionally processing, or standing.",
    "severely_injured": "Do not rewrite as unharmed, recovered, active, or standing/walking.",
    # B-next 확장: cryosleep / asleep / fainted / coma / vegetative_state / restrained / paralyzed / sedated.
    # asleep 은 B-next 에서 legit immobility state — 공통 forbidden list 에 절대 박지 않음.
}

_POSE_LOCKED_BLOCK = (
    "SEMANTIC OVERRIDE:\n"
    "Preserve the fixed pose and body position for affected subjects.\n"
    "Do not introduce a new gesture, standing pose, walking pose, active reaction, or changed limb position.\n"
    "This override takes priority over the sanitizer strategy prefix.\n"
    "Affected entities: {entity_list}."
)

#: 참조 이미지 전용 — **사람이 하나여야 한다** (2026-09-19).
#:
#: 실측: 「다친 10세 아이」가 안전 필터에 막히자 사니타이저가 팩의
#: 「아동 위험 상황 → 보호자와 함께 있는 안전한 장면으로」를 따라
#: `Include the guardian naturally beside her` 를 넣었다. 씬 그림이면
#: 맞는 정책이지만 **참조는 신원을 정하는 그림**이라 사람이 둘이면
#: 하류 샷에서 신원이 섞인다(앞 샷 신원 혼입과 같은 부류).
#:
#: 그래서 금지를 쌓는 대신 **그릴 것을 하나로 못 박는다** — 한 사람.
_REFERENCE_IDENTITY_BLOCK = (
    "SEMANTIC OVERRIDE:\n"
    "This is a REFERENCE image that defines one character's identity.\n"
    "Exactly ONE figure is in frame: {subject}, alone, against a plain "
    "neutral background.\n"
    "This override takes priority over the sanitizer strategy prefix."
)


def _render_semantic_constraints_block(constraints: Optional[Dict[str, Any]]) -> str:
    """user.md {semantic_constraints_block} slot 렌더. constraints=None → 빈 문자열."""
    if not constraints:
        return ""
    lines = [
        "SEMANTIC CONSTRAINTS (이 섹션이 전략 프리픽스와 충돌 시 우선합니다):",
        f"- semantic_mode: {constraints.get('semantic_mode', 'unknown')}",
    ]
    if constraints.get("source_states"):
        lines.append("- 보존 대상 entity 와 상태:")
        for sid, state in sorted(constraints["source_states"].items()):
            lines.append(f"  - {sid}: {state}")
    # IMPORTANT 2 fix — only render True flags. False forbid_* values would
    # contradict Layer 2's _POSE_LOCKED_BLOCK ("active reaction 금지") for
    # pose_locked mode (where router intentionally sets forbid_* = False to
    # avoid character_state over-detection). Mixed signal to LLM blocked.
    for key in (
        "preserve_pose", "preserve_subject_state",
        "forbid_state_polarity_rewrite", "forbid_unharmed_rewrite",
        "forbid_active_reaction",
        # ★참조 이미지는 사람이 하나다 (2026-09-19). 이 칸이 없어서
        #  「아동 → 보호자와 함께」 정책이 참조에도 그대로 먹었다.
        "single_subject_only",
    ):
        if constraints.get(key):
            lines.append(f"- {key}: True")
    return "\n".join(lines)


def _build_immobilized_block(constraints: Dict[str, Any]) -> str:
    source_states = constraints.get("source_states") or {}
    lines = [
        "SEMANTIC OVERRIDE:",
        "Preserve each affected subject's immobilized state and fixed posture.",
        "This override takes priority over the sanitizer strategy prefix.",
        "",
        "Per-entity state contracts:",
    ]
    for sid in sorted(source_states):
        state = source_states[sid]
        guidance = _STATE_GUIDANCE.get(state, "Preserve the structured source state; do not rewrite.")
        lines.append(f"- {sid} ({state}): {guidance}")
    return "\n".join(lines)


def _apply_semantic_override(prompt: str, constraints: Optional[Dict[str, Any]]) -> str:
    """final sanitized_prompt 끝에 deterministic SEMANTIC OVERRIDE block append.

    중복 append 차단: scene_image_pipeline 의 moderation retry 루프 (line 261)
    는 ``current_prompt = sanitize_result.get("sanitized_prompt", ...)`` 로 갱신
    하므로, 다음 retry 의 sanitize() 입력 prompt 에 이미 override block 이 포함
    돼 있다. exact literal marker 기반 검색 (의미 판단 아님) 으로 기존 block
    tail 을 잘라낸 후 새 block append — retry 마다 1 block 만 유지.
    """
    if not constraints or not constraints.get("override_strategy_prefix"):
        return prompt
    mode = constraints.get("semantic_mode")
    if mode == "immobilized":
        block = _build_immobilized_block(constraints)
    elif mode == "pose_locked":
        entity_list = ", ".join(constraints.get("entity_ids") or []) or "(none)"
        block = _POSE_LOCKED_BLOCK.format(entity_list=entity_list)
    elif mode == "reference_identity":
        subject = constraints.get("subject") or "the character described above"
        block = _REFERENCE_IDENTITY_BLOCK.format(subject=subject)
        # 상태가 주어졌으면 **그 상태를 지우지 말라**는 말도 같이 얹는다.
        # `_STATE_GUIDANCE` 는 이미 있던 재료다 — 참조 경로가 안 쓰고 있었다.
        for _sid, _state in sorted(
                (constraints.get("source_states") or {}).items()):
            guide = _STATE_GUIDANCE.get(_state)
            if guide:
                block += f"\n{_sid} stays in the {_state} state. {guide}"
    else:
        return prompt

    idx = prompt.find(_SEMANTIC_OVERRIDE_MARKER)
    if idx >= 0:
        head = prompt[:idx].rstrip()
        return head + "\n\n" + block
    return prompt.rstrip() + "\n\n" + block


def _load_prompt(filename: str) -> str:
    """``prompts/_base/prompt_sanitizer`` 모듈에서 stem 로드 (problems.md #14).

    DB prompt_template 우선 → 파일 fallback (numeric-aware version 정렬). 기존
    ``filename`` 인자 (e.g. ``"sanitize_system.md"``) 호환 위해 ``.md`` 확장자
    자동 제거.

    Raises:
        FileNotFoundError: 모듈/stem 미존재.
        RuntimeError: ``PROMPT_VERSION_PACK_STRICT=true`` 환경에서 stem 이 latest
            version pack 에 없는 경우 (#6 strict mode). caller 가 처리.
    """
    from app.modules.prompt_loader import load_prompt

    stem = filename[:-3] if filename.endswith(".md") else filename
    return load_prompt(_MODULE, stem)


class PromptSanitizer:
    """LLM 클라이언트로 T2I 거절된 프롬프트를 수정하는 모듈."""

    def __init__(self, llm_client=None, project_config: Optional[Dict[str, Any]] = None) -> None:
        # llm_client kept for API compat but no longer used
        self._project_config = project_config

    def sanitize(
        self,
        original_prompt: str,
        block_reason: str,
        block_categories: List[str],
        attempt: int = 1,
        semantic_constraints: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Sanitize a blocked T2I prompt.

        Args:
            original_prompt: The original prompt that was blocked.
            block_reason: Reason for the block (e.g. SAFETY, HARM).
            block_categories: List of safety categories that triggered the block.
            attempt: Sanitization attempt number (1=film_previs, 2=movie_poster, 3=aftermath).
            semantic_constraints: Optional dict from semantic_contract_router. When
                provided, renders a SEMANTIC CONSTRAINTS section into the sanitize LLM's
                user prompt (Layer 1) and appends a SEMANTIC OVERRIDE block to the final
                sanitized_prompt (Layer 2). None → existing behavior unchanged.

        Returns:
            Dict with keys: sanitized_prompt, changes, strategy
        """
        from app.modules.llm.llm_client import call_structured

        strategy_key = ATTEMPT_TO_STRATEGY.get(attempt, "aftermath")
        strategy = STRATEGIES[strategy_key]

        system_prompt = _load_prompt("sanitize_system.md")
        user_prompt = _load_prompt("sanitize_user.md").format(
            original_prompt=original_prompt,
            block_reason=block_reason,
            block_categories=", ".join(block_categories) if block_categories else "N/A",
            attempt=attempt,
            strategy_name=strategy["name"],
            strategy_description=strategy["description"],
            strategy_prefix=strategy["prefix"],
            semantic_constraints_block=_render_semantic_constraints_block(semantic_constraints),
        )

        result = call_structured(
            step="prompt_sanitize",
            system_prompt=system_prompt,
            user_prompt=user_prompt,
            response_schema=SANITIZE_SCHEMA,
            project_config=self._project_config,
            schema_name="prompt_sanitize",
        )

        # Ensure the strategy prefix is included in the sanitized prompt
        sanitized = result.get("sanitized_prompt", "")
        if not sanitized.startswith(strategy["prefix"].strip()[:40]):
            sanitized = strategy["prefix"] + sanitized

        # Patch B-min — final sanitized_prompt 끝에 deterministic SEMANTIC OVERRIDE block append.
        sanitized = _apply_semantic_override(sanitized, semantic_constraints)
        result["sanitized_prompt"] = sanitized

        # Record which strategy was used
        result["strategy"] = strategy_key

        logger.info(
            "Prompt sanitized: attempt=%d, strategy=%s, semantic_mode=%s, changes=%s",
            attempt,
            strategy_key,
            (semantic_constraints or {}).get("semantic_mode") if semantic_constraints else "none",
            result.get("changes", "")[:100],
        )

        return result
