"""experiment_background_prompt_region_ref_preview_slice — W18E.

Deterministic preview of how the W18D `bg_to_fp_region_ref` payload
would appear in a downstream background prompt as an appendix block.
No LLM call. No VLM call. No image API call. No DB / ImageAsset write.
No production manifest mutation. No commit. No push. No new prose is
generated — only labels/hints that already exist in the W18D payload
are carried into a deterministic enumerated string.

Codex W18E contract:
- Input: a prior W18D success run dir.
- For each bg in W18D `bg_to_fp_region_ref_by_bg`:
  - carry `bg_id`, `fp_id`, `base_fp_png_path_relative_to_repo`,
    `target_unit_refs`, `base_marker_refs`,
    `transient_overlay_to_describe` verbatim.
  - assemble `background_prompt_region_appendix` (string) that
    enumerates base layer references (units + base markers) and the
    transient overlay markers separately. The base FP image is
    declared as the layout/room/fixed anchor reference — transient
    markers must never be drawn onto it. Empty transient list →
    fixed sentinel line so consumers can branch deterministically.
  - emit `consumer_decision_preview` enum:
    `READY_FOR_BG_PROMPT_PREVIEW` when a base FP ref path exists,
    `BLOCKED_MISSING_REF` otherwise.

CLI:
  --derive-region-appendix-from <W18D_run_dir>   (required)
  --target-fp-ids fp_l05_01                      (default, wave-1 lock)
  --output-root <path>
  --diag-print-imports
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Set, Tuple

_REPO_ROOT = Path(__file__).resolve().parents[2]
_SCRIPTS_DIR = _REPO_ROOT / "backend" / "scripts"
if str(_SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(_SCRIPTS_DIR))

from experiment_background_pipeline_slice import (  # type: ignore
    KST,
    PLAN_VERSION,
    _check_image_imports_present,
    _check_production_diff_empty,
    _maybe_print_imports,
)
from experiment_floor_plan_grid_layout_slice import (  # type: ignore
    W16_ALLOWED_TARGET_FP_IDS,
    _resolve_targets,
)

W18E_STAGE = "w18e_background_prompt_region_ref_preview_slice"

_DEFAULT_OUTPUT_ROOT = (
    _REPO_ROOT
    / "scripts_output"
    / "background_prompt_region_ref_preview_slice_experiment"
)

_NO_TRANSIENT_SENTINEL = (
    "No transient overlay markers for this background."
)

_DECISION_READY = "READY_FOR_BG_PROMPT_PREVIEW"
_DECISION_BLOCKED = "BLOCKED_MISSING_REF"

_ALLOWED_DECISIONS = {_DECISION_READY, _DECISION_BLOCKED}


def _run_id() -> str:
    import secrets

    return datetime.now(KST).strftime("%Y%m%d_%H%M") + "_" + secrets.token_hex(3)


def _parse_args(argv):
    p = argparse.ArgumentParser(
        description=(
            "W18E background prompt region/ref appendix preview — "
            "deterministic only. No LLM/VLM/image API call. No new prose."
        )
    )
    p.add_argument(
        "--derive-region-appendix-from", required=True,
        help="Path to a prior W18D success run dir.",
    )
    p.add_argument(
        "--target-fp-ids", default="fp_l05_01",
        help="Comma-separated fp_id subset. Wave 1 only allows fp_l05_01.",
    )
    p.add_argument("--output-root", default=str(_DEFAULT_OUTPUT_ROOT))
    p.add_argument("--diag-print-imports", action="store_true")
    return p.parse_args(argv)


def _format_target_unit_line(ref: Dict[str, Any]) -> str:
    n = ref.get("marker_number")
    uid = ref.get("unit_id") or ""
    label = ref.get("label") or ""
    rect = ref.get("approximate_10x10_rect")
    rect_str = (
        "[" + ",".join(str(x) for x in rect) + "]"
        if isinstance(rect, list) and rect
        else "(no rect)"
    )
    return f"  - #{n} ({uid}) {label} @ 10x10 rect {rect_str}"


def _format_base_marker_line(m: Dict[str, Any]) -> str:
    n = m.get("marker_number")
    label = m.get("label") or ""
    decision = m.get("base_layer_decision") or ""
    cell = m.get("approximate_10x10_cell")
    cell_str = (
        "(" + ",".join(str(x) for x in cell) + ")"
        if isinstance(cell, list) and cell
        else "(no cell)"
    )
    return f"  - #{n} {label} [{decision}] @ 10x10 cell {cell_str}"


def _format_transient_line(t: Dict[str, Any]) -> str:
    n = t.get("marker_number")
    label = t.get("label") or ""
    decision = t.get("base_layer_decision") or ""
    uid = t.get("unit_id") or ""
    hint = t.get("overlay_instruction_hint") or ""
    return (
        f"  - #{n} {label} [{decision}] in unit '{uid}' — overlay_hint: "
        f"{hint}"
    )


def _build_appendix_string(*, bg_payload: Dict[str, Any]) -> str:
    base_fp_path = (
        bg_payload.get("base_fp_png_path_relative_to_repo") or ""
    )
    target_unit_refs = bg_payload.get("target_unit_refs") or []
    base_marker_refs = bg_payload.get("base_marker_refs") or []
    transient_overlay = (
        bg_payload.get("transient_overlay_to_describe") or []
    )

    lines: List[str] = []
    lines.append(
        "[background_prompt_region_appendix] "
        "Preview only — assembled deterministically from W18D payload."
    )
    lines.append(
        "Base FP image reference (layout / rooms / fixed anchors only): "
        f"{base_fp_path or '(missing)'}"
    )
    lines.append(
        "Do not redraw, relocate, or add transient overlay markers onto "
        "the base FP image; transient markers belong only in the "
        "background prose."
    )
    lines.append("Target spatial units for this background:")
    if target_unit_refs:
        for ref in target_unit_refs:
            lines.append(_format_target_unit_line(ref))
    else:
        lines.append("  - (none)")

    lines.append("Base markers visible on the base FP image:")
    if base_marker_refs:
        for m in base_marker_refs:
            lines.append(_format_base_marker_line(m))
    else:
        lines.append("  - (none)")

    lines.append("Transient overlay markers to describe in the prose:")
    if transient_overlay:
        for t in transient_overlay:
            lines.append(_format_transient_line(t))
    else:
        lines.append(f"  - {_NO_TRANSIENT_SENTINEL}")

    return "\n".join(lines)


def _decision_for_bg(*, bg_payload: Dict[str, Any]) -> str:
    base_fp_path = (
        bg_payload.get("base_fp_png_path_relative_to_repo") or ""
    )
    if not base_fp_path:
        return _DECISION_BLOCKED
    return _DECISION_READY


def _build_preview_for_fp(
    *, fp_id: str,
    w18d_region_ref_by_bg: Dict[str, Dict[str, Any]],
) -> Tuple[Dict[str, Dict[str, Any]], List[Dict[str, Any]]]:
    """Return ({bg_id: preview_payload}, diagnostics)."""
    out: Dict[str, Dict[str, Any]] = {}
    diagnostics: List[Dict[str, Any]] = []

    for bg_id in sorted(w18d_region_ref_by_bg.keys()):
        bg_payload = w18d_region_ref_by_bg.get(bg_id) or {}
        if (bg_payload.get("fp_id") or "") != fp_id:
            diagnostics.append({
                "bg_id": bg_id,
                "kind": "fp_id_mismatch",
                "expected": fp_id,
                "actual": bg_payload.get("fp_id"),
            })
            continue

        appendix = _build_appendix_string(bg_payload=bg_payload)
        decision = _decision_for_bg(bg_payload=bg_payload)

        out[bg_id] = {
            "bg_id": bg_id,
            "fp_id": fp_id,
            "base_fp_ref_path": (
                bg_payload.get("base_fp_png_path_relative_to_repo") or ""
            ),
            "target_unit_refs": list(
                bg_payload.get("target_unit_refs") or []
            ),
            "base_marker_refs": list(
                bg_payload.get("base_marker_refs") or []
            ),
            "transient_overlay_to_describe": list(
                bg_payload.get("transient_overlay_to_describe") or []
            ),
            "background_prompt_region_appendix": appendix,
            "consumer_decision_preview": decision,
        }

    return out, diagnostics


def _build_w18e_compatibility_report(
    *, preview_by_bg: Dict[str, Dict[str, Any]],
    w18d_region_ref_by_bg: Dict[str, Dict[str, Any]],
    target_fp_ids: Set[str],
    production_diff_empty: bool,
    db_write_count: int,
    image_import_seen: bool,
    image_api_call_count: int,
    vlm_api_call_count: int,
    llm_api_call_count: int,
    missing_inputs: List[str],
    prev_run_id: str,
) -> dict:
    inv: Dict[str, Dict[str, Any]] = {}

    # 1. inputs_present
    inv["inputs_present"] = {
        "pass": (
            not missing_inputs
            and bool(preview_by_bg)
            and bool(w18d_region_ref_by_bg)
        ),
        "detail": {
            "missing_inputs": list(missing_inputs),
            "preview_bg_count": len(preview_by_bg),
            "prev_run_id": prev_run_id,
        },
    }

    # 2. target_fp_only_fp_l05_01
    inv["target_fp_only_fp_l05_01"] = {
        "pass": bool(target_fp_ids) and target_fp_ids.issubset(
            W16_ALLOWED_TARGET_FP_IDS
        ),
        "detail": {
            "received_target_fp_ids": sorted(target_fp_ids or set()),
            "allowed": sorted(W16_ALLOWED_TARGET_FP_IDS),
        },
    }

    # 3. preview_covers_all_w18d_bgs
    expected_bgs = set(w18d_region_ref_by_bg.keys())
    actual_bgs = set(preview_by_bg.keys())
    inv["preview_covers_all_w18d_bgs"] = {
        "pass": expected_bgs == actual_bgs and bool(expected_bgs),
        "detail": {
            "expected_bgs": sorted(expected_bgs),
            "actual_bgs": sorted(actual_bgs),
            "missing_in_w18e": sorted(expected_bgs - actual_bgs),
            "extra_in_w18e": sorted(actual_bgs - expected_bgs),
        },
    }

    # 4. appendix_references_resolve — every marker number referenced in
    # the appendix string must exist in this bg's base_marker_refs OR
    # transient_overlay_to_describe (marker number exact join, not
    # substring meaning).
    import re as _re
    marker_number_pat = _re.compile(r"#(\d+)")
    resolve_failures: List[dict] = []
    for bg_id, payload in preview_by_bg.items():
        allowed_nums: Set[int] = set()
        for ref in payload.get("target_unit_refs") or []:
            v = ref.get("marker_number")
            if isinstance(v, int):
                allowed_nums.add(v)
        for ref in payload.get("base_marker_refs") or []:
            v = ref.get("marker_number")
            if isinstance(v, int):
                allowed_nums.add(v)
        for ref in payload.get("transient_overlay_to_describe") or []:
            v = ref.get("marker_number")
            if isinstance(v, int):
                allowed_nums.add(v)

        appendix = payload.get("background_prompt_region_appendix") or ""
        referenced = {
            int(m.group(1)) for m in marker_number_pat.finditer(appendix)
        }
        unresolved = sorted(referenced - allowed_nums)
        if unresolved:
            resolve_failures.append({
                "bg_id": bg_id,
                "unresolved_marker_numbers_in_appendix": unresolved,
                "allowed_marker_numbers": sorted(allowed_nums),
            })

        decision = payload.get("consumer_decision_preview")
        if decision not in _ALLOWED_DECISIONS:
            resolve_failures.append({
                "bg_id": bg_id,
                "reason": "consumer_decision_preview_invalid_enum",
                "value": decision,
                "allowed": sorted(_ALLOWED_DECISIONS),
            })
    inv["appendix_references_resolve"] = {
        "pass": not resolve_failures,
        "detail": {
            "failures": resolve_failures[:30],
            "failure_count": len(resolve_failures),
        },
    }

    # 5. production / api zero guard
    inv["production_diff_zero_db_write_zero_llm_vlm_image_zero"] = {
        "pass": (
            production_diff_empty
            and db_write_count == 0
            and not image_import_seen
            and image_api_call_count == 0
            and vlm_api_call_count == 0
            and llm_api_call_count == 0
        ),
        "detail": {
            "production_diff_empty": production_diff_empty,
            "db_write_count": db_write_count,
            "image_import_seen": image_import_seen,
            "image_api_call_count": image_api_call_count,
            "vlm_api_call_count": vlm_api_call_count,
            "llm_api_call_count": llm_api_call_count,
        },
    }

    all_pass = all(v["pass"] for v in inv.values())
    return {"invariants": inv, "all_pass": all_pass}


def main(argv=None) -> int:
    args = _parse_args(argv)
    run_id = _run_id()
    out_root = Path(args.output_root)
    run_dir = out_root / run_id
    run_dir.mkdir(parents=True, exist_ok=True)

    prev_run_dir = Path(args.derive_region_appendix_from)
    if not prev_run_dir.is_absolute():
        prev_run_dir = Path.cwd() / prev_run_dir

    target_fp_ids, invalid_targets = _resolve_targets(args.target_fp_ids)

    failed_invariants: List[str] = []
    run_status = "succeeded"
    exit_code = 0

    run_meta: Dict[str, Any] = {
        "run_id": run_id,
        "stage": W18E_STAGE,
        "plan_version": PLAN_VERSION,
        "generated_at": datetime.now(KST).isoformat(),
        "args": vars(args),
        "derived_from": prev_run_dir.name,
        "target_fp_ids": sorted(target_fp_ids),
        "invalid_targets": invalid_targets,
        "llm_api_call_count": 0,
        "vlm_api_call_count": 0,
        "image_api_call_count": 0,
        "outputs": [],
        "run_status": run_status,
        "exit_code": exit_code,
        "failed_invariants": failed_invariants,
    }

    def _persist_and_exit(code: int) -> int:
        run_meta["exit_code"] = code
        if code != 0:
            run_meta["run_status"] = "validation_failed"
        run_meta["failed_invariants"] = failed_invariants
        (run_dir / "run_meta.json").write_text(
            json.dumps(run_meta, ensure_ascii=False, indent=2)
        )
        return code

    missing: List[str] = []
    w18d_region_ref_path = prev_run_dir / "bg_to_fp_region_ref.json"
    w18d_meta_path = prev_run_dir / "run_meta.json"
    if not w18d_region_ref_path.exists():
        missing.append("bg_to_fp_region_ref.json")
    if not w18d_meta_path.exists():
        missing.append("run_meta.json")
    if missing:
        failed_invariants.append("w18d_inputs_missing")
        run_meta["missing_inputs"] = missing
        return _persist_and_exit(1)
    if invalid_targets:
        failed_invariants.append("invalid_target_fp_ids")
        return _persist_and_exit(1)
    if not target_fp_ids.issubset(W16_ALLOWED_TARGET_FP_IDS):
        failed_invariants.append("target_fp_outside_allowed")
        return _persist_and_exit(1)

    w18d_region_ref_doc = json.loads(w18d_region_ref_path.read_text())
    w18d_region_ref_by_bg: Dict[str, Dict[str, Any]] = (
        w18d_region_ref_doc.get("bg_to_fp_region_ref_by_bg") or {}
    )
    fp_id_doc = w18d_region_ref_doc.get("fp_id") or ""

    fp_id_first = sorted(target_fp_ids)[0]
    if fp_id_doc and fp_id_doc != fp_id_first:
        failed_invariants.append("w18d_fp_id_mismatch")
        run_meta["w18d_fp_id"] = fp_id_doc
        return _persist_and_exit(1)

    preview_by_bg, diagnostics = _build_preview_for_fp(
        fp_id=fp_id_first,
        w18d_region_ref_by_bg=w18d_region_ref_by_bg,
    )

    preview_payload = {
        "preview_by_bg": preview_by_bg,
        "fp_id": fp_id_first,
        "derived_from_w18d_run_id": prev_run_dir.name,
        "policy_note": (
            "Preview-only assembly of the background prompt region/ref "
            "appendix. No new prose is generated here; only labels and "
            "hints already present in the W18D payload are enumerated. "
            "Production background_prompt files are not modified."
        ),
        "preview_diagnostics": diagnostics,
    }

    (run_dir / "background_region_ref_prompt_preview.json").write_text(
        json.dumps(preview_payload, ensure_ascii=False, indent=2)
    )
    run_meta["outputs"].append("background_region_ref_prompt_preview.json")

    report = _build_w18e_compatibility_report(
        preview_by_bg=preview_by_bg,
        w18d_region_ref_by_bg=w18d_region_ref_by_bg,
        target_fp_ids=target_fp_ids,
        production_diff_empty=_check_production_diff_empty(),
        db_write_count=0,
        image_import_seen=_check_image_imports_present(),
        image_api_call_count=0,
        vlm_api_call_count=0,
        llm_api_call_count=0,
        missing_inputs=missing,
        prev_run_id=prev_run_dir.name,
    )
    (run_dir / "w18e_compatibility_report.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2)
    )
    run_meta["outputs"].append("w18e_compatibility_report.json")

    for name, v in report["invariants"].items():
        if not v["pass"] and name not in failed_invariants:
            failed_invariants.append(name)
    if failed_invariants and run_status == "succeeded":
        run_status = "validation_failed"
        exit_code = 1

    run_meta["run_status"] = run_status
    run_meta["exit_code"] = exit_code
    run_meta["failed_invariants"] = failed_invariants

    def esc(x):
        return (
            str(x).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
        )

    inv_rows = "".join(
        f"<tr><td>{esc(k)}</td>"
        f"<td class=\"{'pass' if v['pass'] else 'fail'}\">"
        f"{'PASS' if v['pass'] else 'FAIL'}</td>"
        f"<td><pre>{esc(json.dumps(v.get('detail'), ensure_ascii=False))[:800]}</pre></td>"
        f"</tr>"
        for k, v in (report.get("invariants") or {}).items()
    )

    bg_sections: List[str] = []
    for bg_id, payload in preview_by_bg.items():
        target_rows = "".join(
            f"<tr><td>{esc(r.get('unit_id'))}</td>"
            f"<td>#{esc(r.get('marker_number'))}</td>"
            f"<td>{esc(r.get('label'))}</td>"
            f"<td>{esc(r.get('approximate_10x10_rect'))}</td></tr>"
            for r in payload.get("target_unit_refs") or []
        )
        base_rows = "".join(
            f"<tr><td>#{esc(m.get('marker_number'))}</td>"
            f"<td>{esc(m.get('label'))}</td>"
            f"<td>{esc(m.get('base_layer_decision'))}</td>"
            f"<td>{esc(m.get('approximate_10x10_cell'))}</td>"
            f"<td>{esc(m.get('confidence'))}</td></tr>"
            for m in payload.get("base_marker_refs") or []
        )
        transient_rows = "".join(
            f"<tr><td>#{esc(t.get('marker_number'))}</td>"
            f"<td>{esc(t.get('label'))}</td>"
            f"<td>{esc(t.get('unit_id'))}</td>"
            f"<td>{esc(t.get('base_layer_decision'))}</td>"
            f"<td>{esc(t.get('overlay_instruction_hint'))}</td></tr>"
            for t in payload.get("transient_overlay_to_describe") or []
        )
        bg_sections.append(
            f"<section><h2>bg: {esc(bg_id)}</h2>"
            f"<p>base_fp_ref_path: <code>{esc(payload.get('base_fp_ref_path'))}</code></p>"
            f"<p>consumer_decision_preview: <b>{esc(payload.get('consumer_decision_preview'))}</b></p>"
            f"<h3>target_unit_refs</h3>"
            f"<table><tr><th>unit_id</th><th>#</th><th>label</th>"
            f"<th>10x10 rect</th></tr>{target_rows}</table>"
            f"<h3>base_marker_refs</h3>"
            f"<table><tr><th>#</th><th>label</th><th>decision</th>"
            f"<th>cell</th><th>conf</th></tr>{base_rows}</table>"
            f"<h3>transient_overlay_to_describe</h3>"
            f"<table><tr><th>#</th><th>label</th><th>unit_id</th>"
            f"<th>decision</th><th>overlay_hint</th></tr>{transient_rows}"
            f"</table>"
            f"<h3>background_prompt_region_appendix (preview)</h3>"
            f"<pre>{esc(payload.get('background_prompt_region_appendix'))}</pre>"
            f"</section>"
        )

    (run_dir / "index.html").write_text(
        f"""<!doctype html><html><head><meta charset="utf-8">
<title>W18E background prompt region/ref preview {esc(run_id)}</title>
<style>body{{font-family:sans-serif;margin:1.5em}}
table{{border-collapse:collapse;margin:0.5em 0}}
td,th{{border:1px solid #ccc;padding:4px 8px;vertical-align:top}}
.pass{{color:#080}} .fail{{color:#b00}}
pre{{white-space:pre-wrap;font-size:0.85em;max-width:96ch}}
section{{margin:1.5em 0}}</style></head><body>
<h1>W18E — background_prompt region/ref preview {esc(run_id)}</h1>
<p>derived_from(W18D): {esc(prev_run_dir.name)}
| run_status: <b>{esc(run_status)}</b>
| exit_code: {esc(exit_code)}
| llm/vlm/image api calls: <b>0/0/0</b></p>

{''.join(bg_sections)}

<section><h2>invariants</h2>
<table><tr><th>invariant</th><th>status</th><th>detail</th></tr>
{inv_rows}</table></section>

<details><summary>raw run_meta.json</summary>
<pre>{esc(json.dumps(run_meta, ensure_ascii=False, indent=2))}</pre></details>
</body></html>"""
    )
    run_meta["outputs"].append("index.html")

    (run_dir / "run_meta.json").write_text(
        json.dumps(run_meta, ensure_ascii=False, indent=2)
    )
    _maybe_print_imports(args)
    return exit_code


if __name__ == "__main__":
    sys.exit(main())
