"""s16 — nb2 체인 (사용자 지시, 2026-07-05):

  A. **nb2 가 코드 대신 블록 다이어그램 작도** — 그림 중심이 아니라 구조 파악
     전용: 플랫 단색 블록(사각/원)만, 균일 가는 윤곽, 텍스처/음영/3D/원근/상세
     금지. 중요 물체들과 상대적·물리적·논리적 관계에 집중. "코딩처럼 단순한
     블록 구조만". 입력 = 추출 FACTS 텍스트만(레이아웃 참조 이미지 0).
  B. A 를 참조(모사 금지)로 **nb2 가 상세 fp** — LLM 상세 설명(층수 포함)+FACTS
     +실내 fp 드래프팅 스타일 레퍼런스.
  C. B(fp) 를 공간 SOT 로 **실사 부감 2장 — gpt-image-2 / nb2 각각**, 상세 설명
     ·FACTS·장소 데이터 전부 주입.
시나리오 중립: 템플릿은 일반 어휘만, 구체 내용은 데이터(FACTS/desc/loc/override).
사용: .venv/bin/python s16_nb2_chain.py [--only blocks|fp|photos|html]
산출: out/blockset/nb2_{blocks,fp}.png, out/blockset/photo_fromfp_{gpt,nb2}.png,
      plans/nb2_chain_prompts.json, blockgen.html 갱신
"""
import argparse
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import s14_blockgen as S14  # noqa: E402
import s15_fp_redo as S15  # noqa: E402

OUTB = F.OUT / "blockset"


def blocks_prompt(layout):
    return "\n\n".join([
        "\n".join([
            "Draw ONE SCHEMATIC BLOCK DIAGRAM of a real property's layout —",
            "the kind of flat figure a computer program draws: strict",
            "TOP-DOWN, plain solid-colour rectangles and circles on a plain",
            "light background, uniform thin dark outlines. PURPOSE:",
            "structural comprehension only — which elements exist and their",
            "relative sizes, positions and physical/logical relations (what",
            "is beside / inside / connected to what). NOT a picture: no",
            "texture, no materials, no shading, no shadows, no 3D, no",
            "perspective, no artistic detail, no tiny symbols — every",
            "element is ONE plain block (rectangle or circle). Fill colour",
            "encodes ONLY the height layer, in flat steps (darker = higher).",
            "No text, letters, numbers, markers, legend or compass anywhere.",
        ]),
        "LAYOUT RELATIONS (draw exactly these elements and relations):\n"
        + S14.summary_lines(layout, ids=False),
    ])


def fp_prompt(layout, recon, desc):
    return "\n\n".join([
        "\n".join([
            "The FIRST attached image is a rough BLOCK REFERENCE of one real",
            "property: it tells you only WHAT exists and the APPROXIMATE",
            "arrangement. Do NOT trace or copy its composition, block shapes,",
            "proportions or spacing — compose the site plan yourself so the",
            "place reads as a real, believable property that satisfies the",
            "facts below. Its fill colours encode height layers only.",
        ]),
        S15.desc_block(desc),
        S15.facts_block(layout, ids=False),
        S15.STYLE_REF_NOTE,
        S15.STYLE_2D,
    ])


def photo_prompt(layout, recon, desc, ids):
    ov = S14.load_overrides()
    appearance = "\n".join(ov.get("photo_appearance") or [])
    return "\n\n".join(x for x in [
        "\n".join([
            "The FIRST attached image is the property's SITE PLAN — use it",
            "for STRUCTURE ONLY: what exists, counts, relative positions and",
            "connections. It is a flat drawing: its colours, flat fills and",
            "graphic style are NOT materials and must not appear in the",
            "output. Compose the real scene freely from that structure.",
        ]),
        S15.desc_block(desc),
        S15.facts_block(layout, ids=ids),
        "WHAT THIS PLACE LOOKS LIKE (production data):\n"
        + S14.loc_lines(recon),
        appearance,
        "\n".join([
            "Render ONE PHOTOREALISTIC bird's-eye location reference of this",
            "property from a high oblique angle tilted about 30 degrees from",
            "vertical: real built volumes ordered by the described heights",
            "and storeys, with real varied materials per the data — never a",
            "uniform drawing-coloured mass. Plain even daylight, completely",
            "unpopulated, no movable props, no posted papers, no text,",
            "letters, circles, markers or legend anywhere. Never expose",
            "interior rooms.",
        ]),
    ] if x)


def photo_prompt_lived(layout, recon, desc, ids):
    """생활감 변형 — 특정 물품 열거 금지(사용자 규칙): 실주민의 일상 흔적을
    모델이 자유 창작. 사람/가독 텍스트/스토리 특정 소품만 금지."""
    ov = S14.load_overrides()
    appearance = "\n".join(ov.get("photo_appearance") or [])
    return "\n\n".join(x for x in [
        "\n".join([
            "The FIRST attached image is the property's SITE PLAN — use it",
            "for STRUCTURE ONLY: what exists, counts, relative positions and",
            "connections. It is a flat drawing: its colours, flat fills and",
            "graphic style are NOT materials and must not appear in the",
            "output. Compose the real scene freely from that structure.",
        ]),
        S15.desc_block(desc),
        S15.facts_block(layout, ids=ids),
        "WHAT THIS PLACE LOOKS LIKE (production data):\n"
        + S14.loc_lines(recon),
        appearance,
        "\n".join([
            "Render ONE PHOTOREALISTIC bird's-eye location reference of this",
            "property from a high oblique angle tilted about 30 degrees from",
            "vertical: real built volumes ordered by the described heights",
            "and storeys, real varied materials per the data.",
            "No people in frame, no readable text, letters, numbers, markers",
            "or legend anywhere, and nothing tied to any specific story",
            "event. BUT the place must clearly look LIVED-IN: the ordinary",
            "everyday traces that real residents of such a place naturally",
            "leave — you invent them freely; their kind, amount and",
            "placement are your choice, true to how this regional building",
            "type actually looks inhabited. Avoid a sterile, empty,",
            "showroom look. Never expose interior rooms.",
        ]),
    ] if x)


WEBSEARCH_PREAMBLE = "\n".join([
    "Before generating, use web search to ground yourself in how this",
    "regional building type actually looks in reality — its materials,",
    "weathering and the everyday traces its residents leave. Then generate",
    "ONE image that satisfies everything below.",
])


def websearch_photo(prompt, ref_png, out_path):
    """gpt Responses API — web_search + image_generation 도구 결합 (실험)."""
    import base64
    import time
    client = F.openai_client()
    b64 = base64.b64encode(Path(ref_png).read_bytes()).decode("ascii")
    t0 = time.monotonic()
    last = None
    for ws_tool in ("web_search", "web_search_preview"):
        try:
            resp = client.responses.create(
                model="gpt-5.5",
                tools=[{"type": ws_tool},
                       {"type": "image_generation", "size": "1536x1024",
                        "quality": "high"}],
                input=[{"role": "user", "content": [
                    {"type": "input_text",
                     "text": WEBSEARCH_PREAMBLE + "\n\n" + prompt},
                    {"type": "input_image",
                     "image_url": f"data:image/png;base64,{b64}"},
                ]}],
            )
            img_b64 = None
            searched = False
            for item in resp.output:
                t = getattr(item, "type", None)
                if t == "image_generation_call":
                    img_b64 = getattr(item, "result", None)
                if t and "web_search" in str(t):
                    searched = True
            if not img_b64:
                raise RuntimeError("no image_generation_call result in output")
            out_path.write_bytes(base64.b64decode(img_b64))
            F.runlog({"kind": "img", "engine": f"gpt-responses+{ws_tool}",
                      "mode": "search+generate", "tag": "s16_photo_websearch",
                      "refs": [str(ref_png)], "web_search_invoked": searched,
                      "dur_s": round(time.monotonic() - t0, 1),
                      "out": str(out_path), "prompt": prompt})
            print(f"websearch photo OK ({ws_tool}, searched={searched})")
            return out_path
        except Exception as exc:  # 도구 명칭/가용성 차이 — 다음 후보 시도
            last = exc
            F.runlog({"kind": "img_error", "tag": "s16_photo_websearch",
                      "tool": ws_tool, "error": str(exc)[:500]})
    raise RuntimeError(f"websearch photo failed: {last}")


NB2_BLOCKS_LABEL = ("(no layout reference — compose the diagram from the text"
                    " alone)")
NB2_FP_BLOCKS_LABEL = ("BLOCK REFERENCE (what exists + approximate arrangement"
                       " only) — never trace its shapes; fill colours encode"
                       " height layers only.")
NB2_PHOTO_FP_LABEL = ("SITE PLAN (spatial ground truth) — translate to real"
                      " photographic volumes; never copy its drawing style;"
                      " no text anywhere.")


def run(recon, layout, desc, only):
    prompts = {}

    def save():
        F.save_plan("nb2_chain_prompts", prompts)
        S14.build_html(layout, recon)

    fp_style = S14.indoor_fp(recon)
    if only in ("all", "blocks"):
        p = blocks_prompt(layout)
        prompts["nb2_blocks.png"] = p
        F.img_nb2("s16_nb2_blocks", p, [], out_path=OUTB / "nb2_blocks.png")
        save()
    if only in ("all", "fp"):
        p = fp_prompt(layout, recon, desc)
        prompts["nb2_fp.png"] = p
        refs = [(NB2_FP_BLOCKS_LABEL, OUTB / "nb2_blocks.png")]
        if fp_style:
            refs.append((S14.NB2_STYLE_LABEL, fp_style))
        F.img_nb2("s16_nb2_fp", p, refs, out_path=OUTB / "nb2_fp.png")
        save()
    if only == "photos3":
        fp_png = OUTB / "nb2_fp.png"
        pg = photo_prompt_lived(layout, recon, desc, ids=True)
        prompts["photo_fromfp3_gpt.png"] = pg
        F.img_gpt("s16_photo_fromfp3_gpt", pg, refs=[fp_png],
                  out_path=OUTB / "photo_fromfp3_gpt.png")
        save()
        pn = photo_prompt_lived(layout, recon, desc, ids=False)
        prompts["photo_fromfp3_nb2.png"] = pn
        F.img_nb2("s16_photo_fromfp3_nb2", pn,
                  [(NB2_PHOTO_FP_LABEL, fp_png)],
                  out_path=OUTB / "photo_fromfp3_nb2.png")
        save()
        prompts["photo_fromfp3_gptsearch.png"] = pg
        try:
            websearch_photo(pg, fp_png, OUTB / "photo_fromfp3_gptsearch.png")
        except Exception as exc:
            print("websearch variant failed:", exc)
        save()
        return
    if only in ("all", "photos", "photos2"):
        fp_png = OUTB / "nb2_fp.png"
        sfx = "2" if only == "photos2" else ""
        pg = photo_prompt(layout, recon, desc, ids=True)
        prompts[f"photo_fromfp{sfx}_gpt.png"] = pg
        F.img_gpt(f"s16_photo_fromfp{sfx}_gpt", pg, refs=[fp_png],
                  out_path=OUTB / f"photo_fromfp{sfx}_gpt.png")
        save()
        pn = photo_prompt(layout, recon, desc, ids=False)
        prompts[f"photo_fromfp{sfx}_nb2.png"] = pn
        F.img_nb2(f"s16_photo_fromfp{sfx}_nb2", pn,
                  [(NB2_PHOTO_FP_LABEL, fp_png)],
                  out_path=OUTB / f"photo_fromfp{sfx}_nb2.png")
        save()


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", default="all",
                    choices=["all", "blocks", "fp", "photos", "photos2",
                             "photos3", "html"])
    args = ap.parse_args()
    recon = F.load_recon()
    layout = F.load_plan("blockset_layout_v2")
    desc = F.load_plan("fp_place_desc")
    if args.only == "html":
        S14.build_html(layout, recon)
    else:
        run(recon, layout, desc, args.only)
    F.runlog({"kind": "stage", "stage": "s16_nb2_chain", "done": args.only})


if __name__ == "__main__":
    main()
