"""W21B-wave-4 C4: background_prompt projection-card consumer guard.

Deterministic decision the BG prompt consumer makes per bg: whether to
inject the anchor projection card's ``bg_plate_visible_description`` into
the background plate prompt, or fall back to the verified v10 prompt path.

This is the REAL consumer guard for the C2/C3 source_hash + card_id
contract (Codex C4 review Required): the plan node's projection_card_id /
hash / state are NOT trusted blindly. The consumer re-joins the
``shot_projection_card`` checkpoint and verifies the same content-addressed
card_id (== hash) AND the checkpoint's own card_state before injecting.

Fail-closed (wiring brief §6/§9 + Codex C4 lock):
  - pass-only injection. needs_review / blocked / not_available /
    not_applicable -> NO injection (v10 fallback). needs_review is a
    diagnostic-only state in v0; it is never injected.
  - missing checkpoint entry -> projection_card_missing.
  - card_id mismatch (plan built against an older card) ->
    projection_card_stale.
  - checkpoint card_state no longer pass -> projection_card_state_changed.
  - empty plate prose -> projection_card_empty_plate.
  - self-defined internal-token leak in the plate prose ->
    projection_card_leak (reuses the C2 leak check; Korean meaning
    untouched).

Only ``bg_plate_visible_description`` ever reaches the prompt surface —
scene_visible_description / structured visible_items / marker_registry /
card_id / enum literals are never exposed. NO LLM / VLM / image / DB.
"""
from __future__ import annotations

from typing import Any, Dict, Optional, Tuple

from app.modules.pipeline.shot_projection_card import find_plate_prose_leaks

# Plan-level states that carry a real card; only ``pass`` is injectable in v0.
_PASS = "pass"


def _empty_result(*, source_bg_id: str, anchor_shot_id: str, reason: str) -> Dict[str, Any]:
    return {
        "inject": False,
        "plate_prose": "",
        "fallback_reason": reason,
        "source_bg_id": source_bg_id,
        "anchor_shot_id": anchor_shot_id,
        "card_id": "",
    }


def resolve_projection_plate_injection(
    *,
    plan_node: Dict[str, Any],
    card_lookup: Dict[Tuple[str, str], Dict[str, Any]],
) -> Dict[str, Any]:
    """Decide whether to inject a projection card's plate prose for one bg.

    ``plan_node``: the shot_aware_bg_render_plan node (carrying the C3
    projection-card fields). ``card_lookup``: ``{(bg_id, shot_id):
    card_entry}`` re-keyed from the shot_projection_card checkpoint's
    ``data.cards`` (the authoritative card source — the plan node is only a
    pointer).

    Returns ``{inject, plate_prose, fallback_reason, source_bg_id,
    anchor_shot_id, card_id}``. ``inject`` is True only on the fully-verified
    pass path; otherwise ``inject`` is False with an explicit
    ``fallback_reason`` and the caller renders the verified v10 prompt.
    """
    state = plan_node.get("projection_card_state")
    source_bg_id = str(plan_node.get("projection_card_source_bg_id") or "")
    anchor_shot_id = str(plan_node.get("anchor_shot_id") or "")
    expected_card_id = str(plan_node.get("projection_card_id") or "")
    expected_hash = str(plan_node.get("projection_card_hash") or "")

    # 1. pass-only: any non-pass plan state never injects (v10 fallback). The
    #    state itself is the fallback reason (needs_review is diagnostic-only).
    if state != _PASS:
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason=str(state) if state else "no_projection_card_state",
        )

    # 2. plan-node self-consistency (C3 contract: id == hash == card_id). A
    #    missing id/hash, or an id that disagrees with its own hash field,
    #    means the pointer is corrupt / built against a different card — the
    #    consumer must NOT trust it (Codex C4 review Required). Fail closed.
    if not expected_card_id or not expected_hash or expected_card_id != expected_hash:
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_stale",
        )

    # 3. re-join the checkpoint — the plan node's id/hash/state are NOT
    #    trusted; the checkpoint card is the authoritative source.
    entry = card_lookup.get((source_bg_id, anchor_shot_id))
    if not isinstance(entry, dict):
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_missing",
        )

    envelope = entry.get("card") or {}
    actual_card_id = str(envelope.get("card_id") or "")

    # 4. content-addressed id (== hash) must match BOTH the plan node's id and
    #    its hash field — else the plan was built against a different card
    #    version (stale).
    if (
        not actual_card_id
        or actual_card_id != expected_card_id
        or actual_card_id != expected_hash
    ):
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_stale",
        )

    # 5. the checkpoint's OWN card_state is the authoritative gate verdict;
    #    a plan node that says pass while the checkpoint no longer does is a
    #    contract divergence -> fail-closed.
    if entry.get("card_state") != _PASS:
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_state_changed",
        )

    vlm_output = envelope.get("vlm_output") or {}
    plate_prose = vlm_output.get("bg_plate_visible_description")
    if not isinstance(plate_prose, str) or not plate_prose.strip():
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_empty_plate",
        )

    # 6. leakage guard — reuse the C2 self-defined internal-token check on the
    #    plate prose before it reaches the prompt surface (Korean untouched).
    leaks = find_plate_prose_leaks(
        plate_prose=plate_prose,
        vlm_output=vlm_output,
        card_id=actual_card_id,
    )
    if leaks:
        return _empty_result(
            source_bg_id=source_bg_id,
            anchor_shot_id=anchor_shot_id,
            reason="projection_card_leak",
        )

    return {
        "inject": True,
        "plate_prose": plate_prose,
        "fallback_reason": "",
        "source_bg_id": source_bg_id,
        "anchor_shot_id": anchor_shot_id,
        "card_id": actual_card_id,
    }
