"""experiment_background_image_smoke_from_region_ref_slice — W18G.

Single ``L05B04`` smoke for the W18F assembled background prompt.
Issue ONE gpt-image-2 ``images.edit`` call that takes the W18B base FP
PNG as the reference image and the W18F
``assembled_background_prompt_preview`` as the prompt body, with only a
short generic rendering preamble. No retry. No DB write. No
ImageAsset write. No production manifest mutation. No commit. No push.
No second image. No scene-specific prose generation here.

The W18F payload already separates base structural markers from
transient overlay markers; this script does not rewrite the prompt or
add scene-specific text — the rendering preamble is generic only.

CLI:
  --derive-background-smoke-from <W18F_run_dir>   (required)
  --target-bg-id L05B04                           (default, wave lock)
  --generate                                       (default off → dry-run)
  --model gpt-image-2                             (fail-closed)
  --output-root <path>
  --diag-print-imports
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

_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,
    _load_backend_env,
    _maybe_print_imports,
)
from experiment_actual_floor_plan_generation_slice import (  # type: ignore
    _create_openai_client,
    _to_jsonable,
)

W18G_STAGE = "w18g_background_image_smoke_from_region_ref_slice"
W18G_IMAGE_BACKEND = "gpt-image-2"
W18G_IMAGE_SIZE = "1024x1024"
W18G_ALLOWED_BG_IDS = {"L05B04"}

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

_RENDERING_PREAMBLE = (
    "Create an interior background image matching the referenced base "
    "floor plan region; use the FP as layout guidance, not as a "
    "visible overlay. Render the room from a natural standing "
    "eye-level perspective. Do not render any person, body, or "
    "corpse — depict only the room interior, fixtures, and the "
    "transient floor/window/surface cues described below."
)


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=(
            "W18G single background image smoke from W18F assembled "
            "prompt + W18B base FP image reference. images.edit with "
            "gpt-image-2 exactly once when --generate."
        )
    )
    p.add_argument(
        "--derive-background-smoke-from", required=True,
        help="Path to a prior W18F success run dir.",
    )
    p.add_argument(
        "--target-bg-id", default="L05B04",
        help="Single bg_id for this smoke. Wave restricts to L05B04.",
    )
    p.add_argument(
        "--generate", action="store_true",
        help="Issue one gpt-image-2 images.edit call. Default off → dry-run.",
    )
    p.add_argument("--model", default=W18G_IMAGE_BACKEND)
    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 _resolve_w18b_base_fp_png(
    *, w18f_assembly_payload: Dict[str, Any], bg_id: str
) -> Optional[Path]:
    bg_payload = (
        (w18f_assembly_payload or {}).get("assembly_preview_by_bg") or {}
    ).get(bg_id) or {}
    ref_path = bg_payload.get("base_fp_ref_path") or ""
    if not ref_path:
        return None
    p = Path(ref_path)
    if not p.is_absolute():
        p = _REPO_ROOT / ref_path
    return p if p.exists() else None


def _build_smoke_prompt(*, assembled_preview: str) -> str:
    return _RENDERING_PREAMBLE + "\n\n" + assembled_preview


def _edit_png_via_openai_with_reference(
    *, bg_id: str, prompt: str, model: str, size: str,
    base_fp_png_path: Path, target_path: Path, client: Any,
) -> Dict[str, Any]:
    """Single ``images.edit`` call with the base FP PNG as the
    reference image. Returns a JSON-safe result fragment. Never
    raises; failure is surfaced via ``status='api_call_failed'``."""
    import base64

    t0 = time.monotonic()
    try:
        with open(base_fp_png_path, "rb") as f:
            resp = client.images.edit(
                model=model, image=f, prompt=prompt, size=size, n=1,
            )
    except Exception as exc:  # noqa: BLE001
        latency_ms = int((time.monotonic() - t0) * 1000)
        status_code = getattr(exc, "status_code", None)
        return {
            "status": "api_call_failed",
            "png_size_bytes": 0,
            "actual_api_response_meta": {"latency_ms": latency_ms},
            "error_meta": {
                "status_code": status_code,
                "message": str(exc)[:240],
            },
            "cost_meta": {},
        }

    latency_ms = int((time.monotonic() - t0) * 1000)
    data = getattr(resp, "data", None) or []
    if not data:
        return {
            "status": "api_call_failed",
            "png_size_bytes": 0,
            "actual_api_response_meta": {"latency_ms": latency_ms},
            "error_meta": {"status_code": None,
                           "message": "empty response.data"},
            "cost_meta": {},
        }
    first = data[0]
    b64 = getattr(first, "b64_json", None)
    if not b64:
        return {
            "status": "api_call_failed",
            "png_size_bytes": 0,
            "actual_api_response_meta": {"latency_ms": latency_ms},
            "error_meta": {"status_code": None,
                           "message": "response missing b64_json payload"},
            "cost_meta": {},
        }
    target_path.parent.mkdir(parents=True, exist_ok=True)
    target_path.write_bytes(base64.b64decode(b64))

    api_meta: Dict[str, Any] = {"latency_ms": latency_ms}
    for fname in ("created", "request_id", "id", "model"):
        val = getattr(resp, fname, None)
        if val is not None:
            api_meta[fname] = _to_jsonable(val)
    usage = getattr(resp, "usage", None)
    cost_meta: Dict[str, Any] = {}
    if usage is not None:
        cost_meta["provider_usage"] = _to_jsonable(usage)

    return {
        "status": "success",
        "png_size_bytes": target_path.stat().st_size,
        "actual_api_response_meta": api_meta,
        "error_meta": {},
        "cost_meta": cost_meta,
    }


# Module-level caller alias is the test monkeypatch seam.
_openai_edit_caller: Callable = _edit_png_via_openai_with_reference


def _build_w18g_compatibility_report(
    *, inputs_present: bool, target_bg_id: str,
    prompt_text_len: int, base_fp_png_path: Optional[Path],
    mode: str, image_generation_count: int, image_api_call_count: int,
    png_path: Path, production_diff_empty: bool,
    db_write_count: int, image_asset_write_count: int,
    image_import_seen: bool,
) -> dict:
    inv: Dict[str, Dict[str, Any]] = {}

    inv["inputs_present"] = {
        "pass": (
            inputs_present
            and prompt_text_len > 0
            and base_fp_png_path is not None
            and base_fp_png_path.exists()
        ),
        "detail": {
            "prompt_text_len": prompt_text_len,
            "base_fp_png_path": (
                str(base_fp_png_path) if base_fp_png_path else None
            ),
            "base_fp_png_exists": (
                bool(base_fp_png_path and base_fp_png_path.exists())
            ),
        },
    }

    inv["target_bg_only_l05b04"] = {
        "pass": target_bg_id in W18G_ALLOWED_BG_IDS,
        "detail": {
            "target_bg_id": target_bg_id,
            "allowed": sorted(W18G_ALLOWED_BG_IDS),
        },
    }

    inv["prompt_and_fp_ref_present"] = {
        "pass": (
            prompt_text_len > 0
            and base_fp_png_path is not None
            and base_fp_png_path.exists()
        ),
        "detail": {
            "prompt_text_len": prompt_text_len,
            "fp_ref_resolved": bool(
                base_fp_png_path and base_fp_png_path.exists()
            ),
        },
    }

    if mode == "dry_run":
        gen_ok = (
            image_api_call_count == 0 and image_generation_count == 0
            and not png_path.exists()
        )
    else:
        gen_ok = (
            image_api_call_count == 1 and image_generation_count == 1
            and png_path.exists()
        )
    inv["image_generation_count_matches_mode"] = {
        "pass": gen_ok,
        "detail": {
            "mode": mode,
            "image_api_call_count": image_api_call_count,
            "image_generation_count": image_generation_count,
            "png_exists": png_path.exists(),
        },
    }

    inv["production_diff_zero_db_write_zero_no_imageasset_write"] = {
        "pass": (
            production_diff_empty
            and db_write_count == 0
            and image_asset_write_count == 0
            and not image_import_seen
        ),
        "detail": {
            "production_diff_empty": production_diff_empty,
            "db_write_count": db_write_count,
            "image_asset_write_count": image_asset_write_count,
            "image_import_seen": image_import_seen,
        },
    }

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


def main(argv=None) -> int:
    args = _parse_args(argv)
    if args.generate:
        _load_backend_env()

    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_background_smoke_from)
    if not prev_run_dir.is_absolute():
        prev_run_dir = Path.cwd() / prev_run_dir

    failed_invariants: List[str] = []
    run_status = "succeeded"
    exit_code = 0
    mode = "dry_run"
    image_api_call_count = 0
    image_generation_count = 0

    run_meta: Dict[str, Any] = {
        "run_id": run_id,
        "stage": W18G_STAGE,
        "plan_version": PLAN_VERSION,
        "generated_at": datetime.now(KST).isoformat(),
        "args": vars(args),
        "derived_from": prev_run_dir.name,
        "target_bg_id": args.target_bg_id,
        "model": args.model,
        "mode": mode,
        "image_api_call_count": image_api_call_count,
        "image_generation_count": image_generation_count,
        "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

    if args.target_bg_id not in W18G_ALLOWED_BG_IDS:
        failed_invariants.append("target_bg_outside_allowed")
        return _persist_and_exit(1)

    w18f_payload_path = (
        prev_run_dir / "background_prompt_region_ref_assembly_preview.json"
    )
    w18f_meta_path = prev_run_dir / "run_meta.json"
    missing: List[str] = []
    if not w18f_payload_path.exists():
        missing.append("background_prompt_region_ref_assembly_preview.json")
    if not w18f_meta_path.exists():
        missing.append("run_meta.json")
    if missing:
        failed_invariants.append("w18f_inputs_missing")
        run_meta["missing_inputs"] = missing
        return _persist_and_exit(1)

    w18f_payload = json.loads(w18f_payload_path.read_text())
    bg_payload = (
        (w18f_payload or {}).get("assembly_preview_by_bg") or {}
    ).get(args.target_bg_id) or {}
    assembled_preview = (
        bg_payload.get("assembled_background_prompt_preview") or ""
    )

    base_fp_png_path = _resolve_w18b_base_fp_png(
        w18f_assembly_payload=w18f_payload, bg_id=args.target_bg_id,
    )

    prompt_text = (
        _build_smoke_prompt(assembled_preview=assembled_preview)
        if assembled_preview else ""
    )

    png_path = run_dir / "png" / f"{args.target_bg_id}.png"

    result: Dict[str, Any] = {
        "bg_id": args.target_bg_id,
        "fp_id": bg_payload.get("fp_id") or "",
        "model": args.model,
        "size": W18G_IMAGE_SIZE,
        "prompt_text_len_chars": len(prompt_text),
        "rendering_preamble": _RENDERING_PREAMBLE,
        "assembled_preview_carried": bool(assembled_preview),
        "assembled_preview_len_chars": len(assembled_preview),
        "base_fp_png_path": (
            str(base_fp_png_path) if base_fp_png_path else ""
        ),
        "base_fp_png_exists": (
            bool(base_fp_png_path and base_fp_png_path.exists())
        ),
        "png_relative_path": str(png_path.relative_to(run_dir)),
        "status": "dry_run",
    }

    if args.generate:
        if (args.model or "").strip() != W18G_IMAGE_BACKEND:
            failed_invariants.append("model_must_be_gpt_image_2")
            run_meta["model_error"] = (
                f"got {args.model!r}, required {W18G_IMAGE_BACKEND!r}"
            )
            return _persist_and_exit(1)
        if not prompt_text:
            failed_invariants.append("assembled_preview_missing")
            return _persist_and_exit(1)
        if base_fp_png_path is None or not base_fp_png_path.exists():
            failed_invariants.append("base_fp_png_unresolved")
            return _persist_and_exit(1)
        if not os.environ.get("OPENAI_API_KEY"):
            failed_invariants.append("openai_api_key_missing")
            return _persist_and_exit(1)
        try:
            client = _create_openai_client()
        except Exception as exc:  # noqa: BLE001
            failed_invariants.append("openai_client_unavailable")
            run_meta["openai_client_error"] = str(exc)[:240]
            return _persist_and_exit(1)

        mode = "generated"
        image_api_call_count = 1
        outcome = _openai_edit_caller(
            bg_id=args.target_bg_id, prompt=prompt_text, model=args.model,
            size=W18G_IMAGE_SIZE, base_fp_png_path=base_fp_png_path,
            target_path=png_path, client=client,
        )
        result.update(outcome)
        if outcome.get("status") == "success":
            image_generation_count = 1
        else:
            failed_invariants.append("openai_call_failed")
            run_status = "validation_failed"
            exit_code = 1

    (run_dir / "background_image_smoke_result.json").write_text(
        json.dumps({"bg_smoke_result": result}, ensure_ascii=False, indent=2)
    )
    run_meta["outputs"].append("background_image_smoke_result.json")

    production_diff_empty = _check_production_diff_empty()
    image_import_seen = _check_image_imports_present()

    report = _build_w18g_compatibility_report(
        inputs_present=bool(assembled_preview),
        target_bg_id=args.target_bg_id,
        prompt_text_len=len(prompt_text),
        base_fp_png_path=base_fp_png_path,
        mode=mode,
        image_generation_count=image_generation_count,
        image_api_call_count=image_api_call_count,
        png_path=png_path,
        production_diff_empty=production_diff_empty,
        db_write_count=0,
        image_asset_write_count=0,
        image_import_seen=image_import_seen,
    )
    (run_dir / "w18g_compatibility_report.json").write_text(
        json.dumps(report, ensure_ascii=False, indent=2)
    )
    run_meta["outputs"].append("w18g_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["mode"] = mode
    run_meta["image_api_call_count"] = image_api_call_count
    run_meta["image_generation_count"] = image_generation_count
    run_meta["run_status"] = run_status
    run_meta["exit_code"] = exit_code
    run_meta["failed_invariants"] = failed_invariants
    run_meta["png_relative_path"] = result["png_relative_path"]
    run_meta["png_size_bytes"] = (
        png_path.stat().st_size if png_path.exists() else 0
    )

    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))[:700]}</pre></td>"
        f"</tr>"
        for k, v in (report.get("invariants") or {}).items()
    )

    png_html = (
        f'<img src="{esc(result["png_relative_path"])}" '
        f'style="max-width:720px;border:1px solid #ccc"/>'
        if png_path.exists()
        else "<p><i>(no PNG — dry-run or failed call)</i></p>"
    )
    visual_review_html = (
        "<ol>"
        f"<li>Interior reads as the structural unit region targeted by "
        f"the W18F base markers for {esc(args.target_bg_id)} (target "
        f"unit marker numbers carried verbatim from the payload).</li>"
        "<li>Door anchor and window anchor markers reported by W18F "
        "are visible in their referenced positions.</li>"
        "<li>Transient cues from the W18F payload appear as on-surface "
        "floor / window / edge cues only — never drawn back onto the "
        "base FP image overlay.</li>"
        "<li>No person, no body, no corpse rendered. Only the room "
        "interior, fixtures, and the transient cues.</li>"
        "<li>The base FP structural layout is respected; the "
        "generated background does not contradict the FP topology.</li>"
        "</ol>"
    )
    (run_dir / "index.html").write_text(
        f"""<!doctype html><html><head><meta charset=\"utf-8\">
<title>W18G background_image_smoke {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>W18G — background_image_smoke {esc(run_id)}</h1>
<p>mode: <b>{esc(mode)}</b>
| run_status: <b>{esc(run_status)}</b>
| exit_code: {esc(exit_code)}
| model: <b>{esc(args.model)}</b>
| api_call_count: <b>{esc(image_api_call_count)}</b>
| image_generation_count: <b>{esc(image_generation_count)}</b>
| png_size_bytes: <b>{esc(run_meta['png_size_bytes'])}</b>
| derived_from(W18F): <b>{esc(prev_run_dir.name)}</b></p>

<section><h2>Generated PNG</h2>{png_html}</section>

<section><h2>Visual-review checklist (W18G)</h2>{visual_review_html}</section>

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

<details><summary>final smoke prompt</summary>
<pre>{esc(prompt_text)}</pre></details>

<details><summary>background_image_smoke_result.json</summary>
<pre>{esc(json.dumps(result, ensure_ascii=False, indent=2))}</pre></details>
<details><summary>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")
    if png_path.exists():
        run_meta["outputs"].append(str(png_path.relative_to(run_dir)))

    (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())
