"""experiment_actual_floor_plan_generation_slice — W14b.

Consumes a prior W14 candidate_floor_plan_render_slice run and *optionally*
issues actual gpt-image-2 ``images.generate`` calls to render the candidate
floor-plan PNGs. Default mode is **dry-run** (no network, no image bytes
written). Network is only attempted when ``--generate`` is passed and the
user has separately authorised the cost.

W14b never writes to the DB, never registers an ImageAsset, never touches
production code, and never updates a production manifest. Successful PNGs
are written only to the run-local ``<run_dir>/png/<fp_id>.png``.

CLI:
  --derive-actual-render-from <W14_run_dir>   (required)
  --targets all|csv                            (default 'all')
  --generate                                   (default False — dry-run)
  --output-root <path>                         (defaults to scripts_output)
  --diag-print-imports
"""
from __future__ import annotations

import argparse
import json
import shutil
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, 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,
    W6_IMAGE_BACKEND,
    _check_image_imports_present,
    _check_production_diff_empty,
    _load_backend_env,
    _maybe_print_imports,
)

W14B_STAGE = "w14b_actual_floor_plan_generation_slice"
W14B_IMAGE_BACKEND = W6_IMAGE_BACKEND  # "gpt-image-2"

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


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=(
            "W14b actual_floor_plan_generation_slice — dry-run default; "
            "--generate calls gpt-image-2 (cost!) for the chosen --targets."
        )
    )
    p.add_argument(
        "--derive-actual-render-from",
        required=True,
        help="Path to a prior W14 success run dir containing "
             "candidate_floor_plan_render_payloads.json + run_meta.json.",
    )
    p.add_argument(
        "--targets", default="all",
        help="Comma-separated candidate fp_id subset or 'all' (default).",
    )
    p.add_argument(
        "--generate", action="store_true",
        help="Issue real gpt-image-2 calls. Default off → dry-run. "
             "WILL INCUR COST — only set after explicit user approval.",
    )
    p.add_argument(
        "--reuse-existing-png-from-run",
        default=None,
        help="Reuse PNG outputs from a prior W14b run (path or run_id under "
             "actual_floor_plan_generation_slice_experiment/). No network "
             "call, no OpenAI import; PNGs are copied into the new run. "
             "Mutually exclusive with --generate.",
    )
    p.add_argument("--output-root", default=str(_DEFAULT_OUTPUT_ROOT))
    p.add_argument("--diag-print-imports", action="store_true")
    return p.parse_args(argv)


# ─────────────────────────────────────────────────────────────────────────────
# W14 artifact loader
# ─────────────────────────────────────────────────────────────────────────────

def _load_w14_artifacts(prev_run_dir: Path) -> dict:
    required = {
        "payloads": "candidate_floor_plan_render_payloads.json",
        "run_meta": "run_meta.json",
    }
    out: Dict[str, Any] = {}
    missing: List[str] = []
    for key, fname in required.items():
        p = prev_run_dir / fname
        if not p.exists():
            missing.append(fname)
            continue
        out[key] = json.loads(p.read_text())
    out["_missing"] = missing
    out["_prev_run_id"] = prev_run_dir.name
    return out


def _resolve_reuse_source_run_dir(arg_value: str, out_root: Path) -> Optional[Path]:
    """Accept either an absolute/relative path or a bare run_id and resolve
    to a directory that contains a ``png/`` subdirectory. Returns ``None``
    if no candidate has a ``png/`` subdirectory.
    """
    if not arg_value:
        return None
    candidates: List[Path] = []
    raw = Path(arg_value)
    if raw.is_absolute():
        candidates.append(raw)
    else:
        candidates.append(_REPO_ROOT / arg_value)
        candidates.append(Path.cwd() / arg_value)
        candidates.append(out_root / arg_value)
    seen: set = set()
    for c in candidates:
        key = str(c)
        if key in seen:
            continue
        seen.add(key)
        try:
            if c.exists() and (c / "png").is_dir():
                return c
        except OSError:
            continue
    return None


def _resolve_targets(targets_arg: str, payload_ids: set) -> Tuple[set, List[str]]:
    """Parse the ``--targets`` argument. Returns (effective_set, invalid_list).

    - 'all' → full payload set, no invalids.
    - csv  → only entries that are in payload_ids; anything else surfaces in
             invalid_list. The caller decides whether to fail.
    """
    s = (targets_arg or "").strip()
    if s.lower() == "all":
        return set(payload_ids), []
    if not s:
        return set(), []
    requested = [tok.strip() for tok in s.split(",") if tok.strip()]
    valid: set = set()
    invalid: List[str] = []
    for tok in requested:
        if tok in payload_ids:
            valid.add(tok)
        else:
            invalid.append(tok)
    return valid, invalid


# ─────────────────────────────────────────────────────────────────────────────
# JSON-safe conversion. Required because OpenAI SDK objects (e.g.
# response.usage / UsageInputTokensDetails) are nested pydantic BaseModels
# that vars()/__dict__ alone cannot flatten through json.dumps.
# ─────────────────────────────────────────────────────────────────────────────

def _to_jsonable(obj: Any) -> Any:
    if obj is None or isinstance(obj, (bool, int, float, str)):
        return obj
    if isinstance(obj, Path):
        return str(obj)
    if isinstance(obj, dict):
        return {str(k): _to_jsonable(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [_to_jsonable(v) for v in obj]
    if isinstance(obj, (set, frozenset)):
        return sorted(
            (_to_jsonable(v) for v in obj),
            key=lambda x: (str(type(x).__name__), str(x)),
        )
    dump = getattr(obj, "model_dump", None)
    if callable(dump):
        try:
            return _to_jsonable(dump(mode="json"))
        except TypeError:
            try:
                return _to_jsonable(dump())
            except Exception:  # noqa: BLE001
                pass
    to_dict = getattr(obj, "dict", None)
    if callable(to_dict):
        try:
            return _to_jsonable(to_dict())
        except Exception:  # noqa: BLE001
            pass
    if hasattr(obj, "__dict__"):
        try:
            return _to_jsonable(vars(obj))
        except Exception:  # noqa: BLE001
            pass
    return str(obj)


# ─────────────────────────────────────────────────────────────────────────────
# OpenAI wrapper. Imported lazily inside _generate_png_via_openai so the
# module is importable in tests without the SDK installed.
# ─────────────────────────────────────────────────────────────────────────────

def _create_openai_client() -> Any:
    """Construct an OpenAI SDK client with automatic retries disabled.

    Only called when ``--generate`` is set and the env is loaded. The import
    is lazy so test environments without the SDK can still import this
    module and monkeypatch the caller-level wrapper.
    """
    from openai import OpenAI  # type: ignore
    import os

    api_key = os.environ.get("OPENAI_API_KEY") or ""
    if not api_key:
        raise RuntimeError("OPENAI_API_KEY not set; cannot run --generate.")
    try:
        return OpenAI(api_key=api_key, max_retries=0)
    except TypeError:
        # Older SDK without max_retries kwarg. Fall back; we still rely on
        # the caller not to retry on top of the SDK.
        return OpenAI(api_key=api_key)


def _generate_png_via_openai(*, fp_id: str, prompt: str, model: str,
                             size: str, quality: str,
                             target_path: Path,
                             client: Any) -> Dict[str, Any]:
    """Issue a single ``images.generate`` call and write the PNG to
    ``target_path`` on success. Returns a result fragment with status,
    bytes-on-disk, response metadata, and error metadata. Never raises;
    failures are surfaced via ``status='api_call_failed'``.

    This is the test-monkeypatch seam.
    """
    import base64

    t0 = time.monotonic()
    try:
        resp = client.images.generate(
            model=model, prompt=prompt, size=size, quality=quality, 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:
        # Provider-supplied usage carried verbatim (best-effort) through the
        # JSON-safe converter. No local estimated_usd computation per Codex spec.
        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,
    }


# ─────────────────────────────────────────────────────────────────────────────
# Result assembly
# ─────────────────────────────────────────────────────────────────────────────

def _build_results(
    *, payloads: Dict[str, dict],
    effective_targets: set,
    mode: str,
    run_dir: Path,
    openai_caller: Optional[Callable] = None,
    client: Any = None,
    reuse_source_run_dir: Optional[Path] = None,
) -> Tuple[Dict[str, dict], Dict[str, int]]:
    """Per-fp result rows for every W14 payload. Counters: api_call_attempt
    / image_generation / reused_image. dry-run mode never invokes the
    openai_caller; reuse mode copies a prior PNG without any API call.
    """
    results: Dict[str, dict] = {}
    api_call_attempts = 0
    image_generations = 0
    reused_images = 0
    png_dir = run_dir / "png"

    for fp_id, payload in (payloads or {}).items():
        shape = (payload or {}).get("api_call_shape") or {}
        base = {
            "fp_id": fp_id,
            "group_id_pointer": (payload or {}).get("group_id_pointer") or "",
            "prompt": (
                (payload or {}).get("assembled_candidate_diagram_prompt")
                or (payload or {}).get("candidate_diagram_t2i_prompt")
                or ""
            ),
            "api_call_shape_actual": dict(shape),
            "model_used": shape.get("model") or W14B_IMAGE_BACKEND,
            "png_path": "",
            "png_size_bytes": 0,
            "actual_api_response_meta": {},
            "error_meta": {},
            "cost_meta": {},
            "retry_count": 0,
        }

        if fp_id not in effective_targets:
            results[fp_id] = {**base, "status": "target_excluded"}
            continue

        if mode == "dry_run":
            results[fp_id] = {**base, "status": "dry_run_skipped"}
            continue

        if mode == "reuse_existing_png":
            if reuse_source_run_dir is None:
                raise RuntimeError(
                    "reuse_source_run_dir must be provided in reuse_existing_png mode"
                )
            src = reuse_source_run_dir / "png" / f"{fp_id}.png"
            target_path = png_dir / f"{fp_id}.png"
            if not src.exists():
                results[fp_id] = {
                    **base,
                    "status": "api_call_failed",
                    "source_mode": "reused_existing_png",
                    "error_meta": {
                        "status_code": None,
                        "message": f"reuse source PNG missing: {src}",
                    },
                }
                continue
            png_dir.mkdir(parents=True, exist_ok=True)
            shutil.copy2(src, target_path)
            reused_images += 1
            results[fp_id] = {
                **base,
                "status": "success",
                "source_mode": "reused_existing_png",
                "png_path": str(target_path),
                "png_size_bytes": target_path.stat().st_size,
                "actual_api_response_meta": {
                    "reused_from_run": reuse_source_run_dir.name,
                    "provider_metadata_unavailable": True,
                },
                "cost_meta": {},
            }
            continue

        # generate
        if openai_caller is None:
            raise RuntimeError("openai_caller must be provided in generate mode")
        api_call_attempts += 1
        target_path = png_dir / f"{fp_id}.png"
        outcome = openai_caller(
            fp_id=fp_id,
            prompt=base["prompt"],
            model=shape.get("model") or W14B_IMAGE_BACKEND,
            size=shape.get("size") or "1024x1024",
            quality=shape.get("quality") or "high",
            target_path=target_path,
            client=client,
        )
        merged = {**base, **outcome}
        if outcome.get("status") == "success":
            merged["png_path"] = str(target_path)
            merged.setdefault("source_mode", "live_api")
            image_generations += 1
        results[fp_id] = merged

    return results, {
        "api_call_attempt_count": api_call_attempts,
        "image_generation_count": image_generations,
        "reused_image_count": reused_images,
    }


# ─────────────────────────────────────────────────────────────────────────────
# Compatibility report — 5 structural invariants
# ─────────────────────────────────────────────────────────────────────────────

_VALID_STATUSES = {"success", "dry_run_skipped", "target_excluded", "api_call_failed"}


def _build_w14b_compatibility_report(
    *, payloads: Dict[str, dict],
    effective_targets: set,
    invalid_targets: List[str],
    results: Dict[str, dict],
    mode: str,
    api_call_attempt_count: int,
    image_generation_count: int,
    production_diff_empty: bool,
    db_write_count: int,
    image_import_seen: bool,
    image_asset_write_count: int,
    missing_inputs: List[str],
    prev_run_id: str,
) -> dict:
    inv: Dict[str, Dict[str, Any]] = {}

    inv["w14_inputs_present"] = {
        "pass": not missing_inputs and bool(payloads),
        "detail": {
            "missing_inputs": list(missing_inputs),
            "payload_count": len(payloads or {}),
            "prev_run_id": prev_run_id,
        },
    }

    payload_ids = set((payloads or {}).keys())
    out_of_payload = sorted(set(effective_targets) - payload_ids)
    inv["targets_within_payload"] = {
        "pass": (not invalid_targets) and (not out_of_payload),
        "detail": {
            "invalid_targets": list(invalid_targets),
            "effective_target_count": len(effective_targets),
            "out_of_payload_targets": out_of_payload,
        },
    }

    consistent = True
    consistency_detail: Dict[str, Any] = {"mode": mode, "issues": []}
    issues = consistency_detail["issues"]
    if set((results or {}).keys()) != payload_ids:
        consistent = False
        issues.append({
            "kind": "results_set_mismatch",
            "missing": sorted(payload_ids - set(results.keys())),
            "extra": sorted(set(results.keys()) - payload_ids),
        })
    for fp_id, r in (results or {}).items():
        status = r.get("status")
        if status not in _VALID_STATUSES:
            consistent = False
            issues.append({"fp_id": fp_id, "kind": "invalid_status",
                           "status": status})
            continue
        if fp_id not in effective_targets and status != "target_excluded":
            consistent = False
            issues.append({"fp_id": fp_id, "kind": "excluded_status_mismatch",
                           "status": status})
        if fp_id in effective_targets and mode == "dry_run" and status != "dry_run_skipped":
            consistent = False
            issues.append({"fp_id": fp_id, "kind": "dry_run_status_mismatch",
                           "status": status})
    if mode == "dry_run":
        if api_call_attempt_count != 0 or image_generation_count != 0:
            consistent = False
            issues.append({"kind": "dry_run_counters_nonzero",
                           "api_call_attempt_count": api_call_attempt_count,
                           "image_generation_count": image_generation_count})
    elif mode == "generate":
        if api_call_attempt_count != len(effective_targets):
            consistent = False
            issues.append({"kind": "generate_attempt_count_mismatch",
                           "expected": len(effective_targets),
                           "got": api_call_attempt_count})
    elif mode == "reuse_existing_png":
        if api_call_attempt_count != 0:
            consistent = False
            issues.append({"kind": "reuse_api_call_attempt_nonzero",
                           "api_call_attempt_count": api_call_attempt_count})
        for fp_id in effective_targets:
            r = (results or {}).get(fp_id, {})
            if r.get("status") != "success":
                consistent = False
                issues.append({"fp_id": fp_id, "kind": "reuse_targeted_not_success",
                               "status": r.get("status")})
                continue
            if r.get("source_mode") != "reused_existing_png":
                consistent = False
                issues.append({"fp_id": fp_id, "kind": "reuse_source_mode_mismatch",
                               "source_mode": r.get("source_mode")})
            png_path = r.get("png_path") or ""
            if not png_path or not Path(png_path).exists():
                consistent = False
                issues.append({"fp_id": fp_id, "kind": "reuse_png_missing_on_disk",
                               "png_path": png_path})
    inv["all_targeted_results_present_and_mode_consistent"] = {
        "pass": consistent,
        "detail": consistency_detail,
    }

    wrong_model = [
        fp_id for fp_id, r in (results or {}).items()
        if r.get("status") == "success" and r.get("model_used") != W14B_IMAGE_BACKEND
    ]
    inv["model_used_was_gpt_image_2"] = {
        "pass": not wrong_model,
        "detail": {
            "wrong_model_fp_ids": sorted(wrong_model),
            "expected": W14B_IMAGE_BACKEND,
        },
    }

    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,
        },
    }

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


# ─────────────────────────────────────────────────────────────────────────────
# HTML render
# ─────────────────────────────────────────────────────────────────────────────

def _render_w14b_html(run_meta: dict, results: Dict[str, dict],
                      payloads: Dict[str, dict], report: dict,
                      run_dir: Path) -> None:
    def esc(x):
        return (str(x).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))

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

    fp_rows = ""
    for fp_id, r in results.items():
        status = r.get("status")
        if status == "success":
            embed_cell = (
                f"<img src=\"png/{esc(fp_id)}.png\" width=\"256\" "
                f"alt=\"{esc(fp_id)}\">"
            )
        elif status == "dry_run_skipped":
            embed_cell = "<small>(dry-run: no image)</small>"
        elif status == "target_excluded":
            embed_cell = "<small>(excluded by --targets)</small>"
        elif status == "api_call_failed":
            err = r.get("error_meta") or {}
            embed_cell = (
                "<small class=\"fail\">api_call_failed "
                f"status={esc(err.get('status_code'))}<br>"
                f"{esc((err.get('message') or '')[:160])}</small>"
            )
        else:
            embed_cell = "<small>(unknown)</small>"

        shape = r.get("api_call_shape_actual") or {}
        shape_cell = (
            f"method={esc(shape.get('client_method'))} "
            f"model={esc(shape.get('model'))} "
            f"size={esc(shape.get('size'))} "
            f"quality={esc(shape.get('quality'))} "
            f"n={esc(shape.get('n'))}"
        )
        api_meta = r.get("actual_api_response_meta") or {}
        cost_meta = r.get("cost_meta") or {}
        fp_rows += (
            f"<tr><td>{esc(fp_id)}</td>"
            f"<td>{esc(r.get('group_id_pointer'))}</td>"
            f"<td>{esc(status)}</td>"
            f"<td>{esc(r.get('model_used'))}</td>"
            f"<td><pre>{esc(r.get('png_path') or '')}</pre>"
            f"<small>bytes={esc(r.get('png_size_bytes'))}</small></td>"
            f"<td>{embed_cell}</td>"
            f"<td><small>{shape_cell}</small></td>"
            f"<td><pre>{esc(json.dumps(api_meta, ensure_ascii=False))[:240]}</pre></td>"
            f"<td><pre>{esc(json.dumps(cost_meta, ensure_ascii=False))[:240]}</pre></td></tr>"
        )

    html = f"""<!doctype html><html><head><meta charset=\"utf-8\">
<title>W14b actual_floor_plan_generation_slice {esc(run_meta.get('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:60ch}}
section{{margin:1.5em 0}}
img{{display:block;max-width:256px;height:auto}}</style></head>
<body>
<h1>W14b — actual_floor_plan_generation_slice {esc(run_meta.get('run_id'))}</h1>
<p>stage: <b>{esc(run_meta.get('stage'))}</b>
| mode: <b>{esc(run_meta.get('mode'))}</b>
| run_status: <b>{esc(run_meta.get('run_status'))}</b>
| exit_code: {esc(run_meta.get('exit_code'))}
| derived_from(W14): {esc(run_meta.get('derived_from'))}
| api_call_attempt_count: <b>{esc(run_meta.get('api_call_attempt_count'))}</b>
| image_generation_count: <b>{esc(run_meta.get('image_generation_count'))}</b>
| image_generation_backend: <b>{esc(run_meta.get('image_generation_backend'))}</b></p>

<section><h2>1. Per-fp actual generation results</h2>
<table><tr>
<th>fp_id</th><th>group_id_pointer</th><th>status</th>
<th>model_used</th><th>png_path</th><th>embed</th>
<th>api_call_shape_actual</th><th>actual_api_response_meta</th><th>cost_meta</th>
</tr>{fp_rows}</table></section>

<section><h2>2. 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>
<details><summary>raw results.json</summary>
<pre>{esc(json.dumps(results, ensure_ascii=False, indent=2))[:200000]}</pre></details>
</body></html>"""
    (run_dir / "index.html").write_text(html)


# ─────────────────────────────────────────────────────────────────────────────
# main
# ─────────────────────────────────────────────────────────────────────────────

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

    artifacts = _load_w14_artifacts(prev_run_dir)
    missing = list(artifacts.get("_missing", []))
    outputs: List[str] = []
    failed: List[str] = []

    reuse_source_run_dir: Optional[Path] = None
    if args.reuse_existing_png_from_run and args.generate:
        sys.stderr.write(
            "[W14b] --reuse-existing-png-from-run is mutually exclusive with "
            "--generate.\n"
        )
        return 2
    if args.reuse_existing_png_from_run:
        reuse_source_run_dir = _resolve_reuse_source_run_dir(
            args.reuse_existing_png_from_run, out_root
        )
        if reuse_source_run_dir is None:
            sys.stderr.write(
                f"[W14b] reuse source run dir not found: "
                f"{args.reuse_existing_png_from_run}\n"
            )
            return 2
        mode = "reuse_existing_png"
    elif args.generate:
        mode = "generate"
    else:
        mode = "dry_run"

    run_meta: Dict[str, Any] = {
        "run_id": run_id,
        "stage": W14B_STAGE,
        "plan_version": PLAN_VERSION,
        "generated_at": datetime.now(KST).isoformat(),
        "mode": mode,
        "api_call_attempt_count": 0,
        "image_generation_count": 0,
        "reused_image_count": 0,
        "image_generation_backend": W14B_IMAGE_BACKEND,
        "args": vars(args),
        "derived_from": prev_run_dir.name,
        "reuse_source_run_id": reuse_source_run_dir.name if reuse_source_run_dir else None,
        "outputs": outputs,
        "run_status": "unknown",
        "exit_code": 0,
        "failed_invariants": failed,
    }

    if missing:
        failed.append("w14_inputs_missing")
        run_meta["run_status"] = "validation_failed"
        run_meta["exit_code"] = 1
        run_meta["failed_invariants"] = failed
        (run_dir / "run_meta.json").write_text(
            json.dumps(_to_jsonable(run_meta), ensure_ascii=False, indent=2)
        )
        return 1

    payloads = (artifacts["payloads"].get("payloads") or {})
    payload_ids = set(payloads.keys())

    effective_targets, invalid_targets = _resolve_targets(args.targets, payload_ids)

    if invalid_targets or not effective_targets:
        # Render report + HTML so the caller still has a record, but fail.
        results = {
            fp_id: {
                "fp_id": fp_id, "status": "target_excluded",
                "group_id_pointer": (payloads.get(fp_id) or {}).get("group_id_pointer") or "",
                "prompt": "", "api_call_shape_actual": (payloads.get(fp_id) or {}).get("api_call_shape") or {},
                "model_used": ((payloads.get(fp_id) or {}).get("api_call_shape") or {}).get("model") or W14B_IMAGE_BACKEND,
                "png_path": "", "png_size_bytes": 0,
                "actual_api_response_meta": {}, "error_meta": {},
                "cost_meta": {}, "retry_count": 0,
            }
            for fp_id in payload_ids
        }
        report = _build_w14b_compatibility_report(
            payloads=payloads, effective_targets=effective_targets,
            invalid_targets=invalid_targets, results=results, mode=mode,
            api_call_attempt_count=0, image_generation_count=0,
            production_diff_empty=_check_production_diff_empty(),
            db_write_count=0, image_import_seen=_check_image_imports_present(),
            image_asset_write_count=0, missing_inputs=missing,
            prev_run_id=prev_run_dir.name,
        )
        (run_dir / "w14b_compatibility_report.json").write_text(
            json.dumps(_to_jsonable(report), ensure_ascii=False, indent=2)
        )
        outputs.append("w14b_compatibility_report.json")
        (run_dir / "actual_floor_plan_render_results.json").write_text(
            json.dumps({"results": _to_jsonable(results)}, ensure_ascii=False, indent=2)
        )
        outputs.append("actual_floor_plan_render_results.json")
        for name, v in report["invariants"].items():
            if not v["pass"] and name not in failed:
                failed.append(name)
        run_meta["run_status"] = "validation_failed"
        run_meta["exit_code"] = 1
        run_meta["failed_invariants"] = failed
        _render_w14b_html(run_meta, results, payloads, report, run_dir)
        outputs.append("index.html")
        run_meta["outputs"] = outputs
        (run_dir / "run_meta.json").write_text(
            json.dumps(_to_jsonable(run_meta), ensure_ascii=False, indent=2)
        )
        return 1

    # Real generate branch: load env + client. Imports happen ONLY here so
    # dry-run and reuse modes never import the OpenAI SDK.
    client: Any = None
    if mode == "generate":
        sys.stderr.write(
            f"[W14b] WILL ATTEMPT {len(effective_targets)} target(s) with "
            f"gpt-image-2; no automatic retry; output run-local only.\n"
        )
        _load_backend_env()
        client = _create_openai_client()

    results, counters = _build_results(
        payloads=payloads, effective_targets=effective_targets,
        mode=mode, run_dir=run_dir,
        openai_caller=_generate_png_via_openai if mode == "generate" else None,
        client=client,
        reuse_source_run_dir=reuse_source_run_dir,
    )
    (run_dir / "actual_floor_plan_render_results.json").write_text(
        json.dumps({"results": _to_jsonable(results)}, ensure_ascii=False, indent=2)
    )
    outputs.append("actual_floor_plan_render_results.json")

    report = _build_w14b_compatibility_report(
        payloads=payloads, effective_targets=effective_targets,
        invalid_targets=invalid_targets, results=results, mode=mode,
        api_call_attempt_count=counters["api_call_attempt_count"],
        image_generation_count=counters["image_generation_count"],
        production_diff_empty=_check_production_diff_empty(),
        db_write_count=0,
        image_import_seen=_check_image_imports_present(),
        image_asset_write_count=0,
        missing_inputs=missing,
        prev_run_id=prev_run_dir.name,
    )
    (run_dir / "w14b_compatibility_report.json").write_text(
        json.dumps(_to_jsonable(report), ensure_ascii=False, indent=2)
    )
    outputs.append("w14b_compatibility_report.json")

    for name, v in report["invariants"].items():
        if not v["pass"] and name not in failed:
            failed.append(name)

    if failed:
        run_status = "validation_failed"
        exit_code = 1
    elif mode == "dry_run":
        run_status = "dry_run"
        exit_code = 0
    elif mode == "reuse_existing_png":
        any_reuse_failed = any(
            results[fp_id]["status"] != "success"
            for fp_id in effective_targets if fp_id in results
        )
        run_status = "partial_failed" if any_reuse_failed else "succeeded"
        exit_code = 1 if any_reuse_failed else 0
    else:
        any_failed_target = any(
            results[fp_id]["status"] == "api_call_failed"
            for fp_id in effective_targets if fp_id in results
        )
        if any_failed_target:
            run_status = "partial_failed"
            exit_code = 1
        else:
            run_status = "succeeded"
            exit_code = 0

    run_meta["api_call_attempt_count"] = counters["api_call_attempt_count"]
    run_meta["image_generation_count"] = counters["image_generation_count"]
    run_meta["reused_image_count"] = counters.get("reused_image_count", 0)
    run_meta["run_status"] = run_status
    run_meta["exit_code"] = exit_code
    run_meta["failed_invariants"] = failed
    run_meta["outputs"] = outputs
    _render_w14b_html(run_meta, results, payloads, report, run_dir)
    outputs.append("index.html")
    run_meta["outputs"] = outputs
    (run_dir / "run_meta.json").write_text(
        json.dumps(_to_jsonable(run_meta), ensure_ascii=False, indent=2)
    )
    _maybe_print_imports(args)
    return exit_code


if __name__ == "__main__":
    raise SystemExit(main())
