"""s23 — 지역 지도 스타일 베이스 + 촬영지만 fp 스케치 (사용자 아이디어,
2026-07-07).

  - 베이스 = s22 관계 데이터(결정론 검증 통과 좌표)를 **지도 문법**으로
    결정론 PIL 렌더: 도로/골목=흰 띠+회색 케이싱, 이웃=무마커 회색 블록,
    촬영지(마커 요소)=옅은 채움+원형 문자+주황 파선 경계 하이라이트.
  - i2i = 촬영지 영역만 상세 fp 스케치로 재작도(gpt edit/nb2), 지도 부분
    은 원본 유지 — fp 어휘=기존 S15.STYLE_2D 계약 재사용, 마커=지정 원형
    문자만(⑯-2 계약 어휘).
사용: backend/.venv/bin/python s23_map_fp.py [--only all|base|sketch|html]
산출: out/blockset/bd_map_base.png + bd_map_fp_{gpt,nb2}.png, map_fp.html
"""
import argparse
import html as _html
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
import forest_lib as F  # noqa: E402
import s13_blockset_v2 as S13  # noqa: E402
import s15_fp_redo as S15  # noqa: E402  (STYLE_2D fp 어휘 재사용)

OUTB = F.OUT / "blockset"
PAGE = F.EXP / "map_fp.html"
CONTEXT_KINDS = {"road", "alley", "neighbor_mass"}

BASE = "bd_map_base.png"
SK_GPT = "bd_map_fp_gpt.png"
SK_NB2 = "bd_map_fp_nb2.png"

# fp 스케치 계약 — 지도 보존 + 촬영지만 fp 재작도 (기존 어휘 재사용)
MAP_FP_NOTE = "\n".join([
    "The attached image is a NEIGHBOURHOOD MAP around one filming",
    "location, drawn straight-down. The filming property is the area",
    "inside the dashed boundary, whose parts carry circled letter",
    "markers. REDRAW ONLY that property area as a detailed top-down",
    "architectural floor-plan sketch, keeping every block's position,",
    "size, proportions and boundary openings exactly where the map",
    "draws them.",
    "EVERYTHING OUTSIDE the dashed boundary (roads, unmarked",
    "neighbour blocks, background) stays in the original simple flat",
    "MAP style, completely unchanged.",
    "Keep each circled letter marker exactly in place; apart from",
    "these circled letters add no text, no words, no numbers, no",
    "legend, no compass.",
    "STRICT VERTICAL TOP-DOWN ONLY — no 3D, no axonometric tilt, no",
    "facades, no shadows.",
])


# v2 (1차 판정: 정체 미주입 → gpt=정원 저택 발명 / nb2=부지 전체 실내화):
# 마커↔정체 KEY ELEMENTS 주입 + 실외 봉인 조항
MAP_FP_EXTERIOR = "\n".join([
    "This property is an OUTDOOR urban site: open areas (yard,",
    "terrace, roof surfaces, alley) are OPEN-AIR ground/roof",
    "surfaces — never rooms; do NOT wrap them in walls, do NOT add",
    "interior partitions, furniture, gardens, lawns, fountains or",
    "planting that is not listed below. Every enclosed unit stays",
    "sealed — outer shell and its listed openings only.",
])


def sketch_prompt(layout, rel):
    style = S15.STYLE_2D
    # fp 어휘(제도 밀도)만 차용 — no-text 줄은 MAP_FP_NOTE 가 관할
    core = style.split("Neutral document:")[0].strip()
    mk = {e.get("_id"): e["marker"] for e in layout["elements"]}
    ident = [f"{mk[e['id']]}. {e['name_en']} ({e['kind']}) — {e['desc_ko']}"
             for e in rel["elements"]
             if e["kind"] not in CONTEXT_KINDS and e["id"] in mk]
    return "\n\n".join([
        MAP_FP_NOTE,
        MAP_FP_EXTERIOR,
        "KEY ELEMENTS (identity of each marked part — markers are"
        " identifiers only, draw each part as WHAT IT IS):\n"
        + "\n".join(ident),
        "Floor-plan drafting language for the property area:\n" + core,
    ])


# ── 무참조 경로 (사용자 정정: "코드 렌더를 참조로 넣는 게 아니라, 아무것도
#    없는 것에 지도처럼 그리게 하고 → 그 위에 i2i 로 촬영지만 상세화") ──
PURE_MAP_GPT = "map_pure_gpt.png"
PURE_MAP_NB2 = "map_pure_nb2.png"
PURE_FP_GPT = "mapfp_pure_gpt.png"
PURE_FP_NB2 = "mapfp_pure_nb2.png"

MAP_STYLE_NOTE = "\n".join([
    "Draw ONE straight-down NEIGHBOURHOOD MAP in clean digital",
    "street-map language: white roads with thin grey casings, flat",
    "pale building blocks on a paper-tone background, strictly 2D",
    "top-down — no 3D, no perspective, no shadows, no labels, no",
    "text, no icons, no compass.",
    "The FILMING PROPERTY parcel is the subject: draw its parts per",
    "the STRUCTURE lines below, and outline the parcel once with a",
    "thin dashed boundary. Surrounding context stays simple: plain",
    "full-scale neighbouring blocks and the approach road/alley only",
    "(edge blocks may be cut by the frame).",
])

MAP_FP_NOTE_PURE = "\n".join([
    "The attached image is a NEIGHBOURHOOD MAP around one filming",
    "location, drawn straight-down. The filming property is the area",
    "inside the dashed boundary. REDRAW ONLY that property area as a",
    "detailed top-down architectural floor-plan sketch, keeping every",
    "block's position, size, proportions and openings exactly where",
    "the map draws them.",
    "EVERYTHING OUTSIDE the dashed boundary (roads, neighbour blocks,",
    "background) stays in the original simple flat MAP style,",
    "completely unchanged.",
    "Add no text, no words, no numbers, no legend, no compass.",
    "STRICT VERTICAL TOP-DOWN ONLY — no 3D, no axonometric tilt, no",
    "facades, no shadows.",
])


def _minimal_lines():
    res = F.load_plan("numbered_bd_v1")
    return "\n".join(res["numbered_structure_en"])


def pure_map_prompt():
    return "\n\n".join([
        MAP_STYLE_NOTE,
        "STRUCTURE (the property and its surroundings):\n"
        + _minimal_lines(),
    ])


def pure_fp_prompt():
    style = S15.STYLE_2D
    core = style.split("Neutral document:")[0].strip()
    return "\n\n".join([
        MAP_FP_NOTE_PURE,
        MAP_FP_EXTERIOR,
        "KEY ELEMENTS (identity of the property's parts — draw each as"
        " WHAT IT IS, positions per the map):\n" + _minimal_lines(),
        "Floor-plan drafting language for the property area:\n" + core,
    ])


def run_pure():
    pm = pure_map_prompt()
    pf = pure_fp_prompt()
    try:
        prompts = F.load_plan("spatial_prompts")
    except Exception:
        prompts = {}
    for fn, p in ((PURE_MAP_GPT, pm), (PURE_MAP_NB2, pm),
                  (PURE_FP_GPT, pf), (PURE_FP_NB2, pf)):
        prompts[fn] = p
    F.save_plan("spatial_prompts", prompts)
    build_page()
    # step 1 — 무참조 T2I 지도 (엔진별 lane)
    F.img_gpt("s23_pure_map_gpt", pm, refs=None,
              out_path=OUTB / PURE_MAP_GPT)
    build_page()
    F.img_nb2("s23_pure_map_nb2", pm, [], out_path=OUTB / PURE_MAP_NB2)
    build_page()
    # step 2 — 같은 lane 지도 위 i2i 촬영지 상세화
    F.img_gpt("s23_pure_fp_gpt", pf, refs=[OUTB / PURE_MAP_GPT],
              out_path=OUTB / PURE_FP_GPT)
    build_page()
    F.img_nb2("s23_pure_fp_nb2", pf,
              [("NEIGHBOURHOOD MAP with the filming property inside the"
                " dashed boundary — redraw ONLY the property area as a"
                " floor-plan sketch per the prompt; keep the map part"
                " unchanged.", OUTB / PURE_MAP_NB2)],
              out_path=OUTB / PURE_FP_NB2)
    build_page()


def load_solved():
    plan = F.load_plan("bd_code_rel_v1")
    r = plan["rounds"][-1]
    assert not r["violations"], "마지막 라운드에 위반 잔존 — s22 먼저 수렴"
    return r["layout"], r["rel"]


def render_map(layout, out_path):
    """결정론 지도 스타일 렌더 — 좌표=s22 솔버 산출 그대로."""
    from PIL import Image, ImageDraw, ImageFont
    S, M = 11, 46
    W, H = int(100 * S + M * 2), int(100 * S + M * 2 + 36)
    img = Image.new("RGB", (W, H), "#f4f1ea")
    d = ImageDraw.Draw(img)

    def font(sz, bold=False):
        for p in ("/System/Library/Fonts/Helvetica.ttc",
                  "/System/Library/Fonts/Supplemental/Arial.ttf"):
            try:
                return ImageFont.truetype(p, sz, index=1 if bold else 0)
            except Exception:
                continue
        return ImageFont.load_default()

    def px(x, y):
        return M + x * S, M + 30 + y * S

    def rect_px(e):
        if e["shape"] == "circle":
            x, y = e["x"] - e["w"] / 2, e["y"] - e["h"] / 2
        else:
            x, y = e["x"], e["y"]
        return [px(x, y), px(x + e["w"], y + e["h"])]

    els = layout["elements"]
    ctx = [e for e in els if e["kind"] in CONTEXT_KINDS]
    prop = [e for e in els if e["kind"] not in CONTEXT_KINDS]

    # 1) 도로/골목 = 지도식 흰 띠 + 케이싱
    for e in ctx:
        if e["kind"] in ("road", "alley"):
            d.rectangle(rect_px(e), fill="#ffffff", outline="#c9c4b8",
                        width=3)
    # 2) 이웃 = 무마커 회색 블록
    for e in ctx:
        if e["kind"] == "neighbor_mass":
            d.rectangle(rect_px(e), fill="#e6e2d8", outline="#cfc9bb",
                        width=2)
    # 3) 촬영지 블록 = 옅은 채움 (스케치 여지) + 얇은 윤곽
    blocks = [e for e in prop if e["kind"] in S13.BLOCK_KINDS]
    points = [e for e in prop if e["kind"] not in S13.BLOCK_KINDS]
    blocks.sort(key=lambda e: -(e["w"] * e["h"]))
    for e in blocks:
        if e["shape"] == "circle":
            d.ellipse(rect_px(e), fill="#fdfbf4", outline="#8a8578", width=2)
        else:
            d.rectangle(rect_px(e), fill="#fdfbf4", outline="#8a8578",
                        width=2)
        if e["kind"] == "stair":
            x0, y0 = e["x"], e["y"]
            n = max(4, int(max(e["w"], e["h"])))
            for i in range(1, n):
                t = i / n
                if e["w"] >= e["h"]:
                    d.line([px(x0 + e["w"] * t, y0),
                            px(x0 + e["w"] * t, y0 + e["h"])],
                           fill="#8a8578", width=1)
                else:
                    d.line([px(x0, y0 + e["h"] * t),
                            px(x0 + e["w"], y0 + e["h"] * t)],
                           fill="#8a8578", width=1)
    for e in points:
        if e["shape"] == "circle":
            d.ellipse(rect_px(e), fill="#f3efe4", outline="#8a8578", width=2)
        else:
            d.rectangle(rect_px(e), fill="#f3efe4", outline="#8a8578",
                        width=2)

    # 4) 촬영지 경계 하이라이트 = 주황 파선 bbox
    xs, ys, xe, ye = 1e9, 1e9, -1e9, -1e9
    for e in prop:
        if e["shape"] == "circle":
            x, y = e["x"] - e["w"] / 2, e["y"] - e["h"] / 2
        else:
            x, y = e["x"], e["y"]
        xs, ys = min(xs, x), min(ys, y)
        xe, ye = max(xe, x + e["w"]), max(ye, y + e["h"])
    pad = 1.6
    (bx0, by0), (bx1, by1) = px(xs - pad, ys - pad), px(xe + pad, ye + pad)
    step, on = 22, 13
    x = bx0
    while x < bx1:
        d.line([(x, by0), (min(x + on, bx1), by0)], fill="#d2691e", width=4)
        d.line([(x, by1), (min(x + on, bx1), by1)], fill="#d2691e", width=4)
        x += step
    y = by0
    while y < by1:
        d.line([(bx0, y), (bx0, min(y + on, by1))], fill="#d2691e", width=4)
        d.line([(bx1, y), (bx1, min(y + on, by1))], fill="#d2691e", width=4)
        y += step

    # 5) 원형 문자 마커 = 촬영지 요소만
    f_mark = font(15, True)
    for e in prop:
        if e["shape"] == "circle":
            mx, my = px(e["x"], e["y"])
        else:
            mx, my = px(e["x"] + e["w"] / 2, e["y"] + e["h"] / 2)
        r = 13
        d.ellipse([mx - r, my - r, mx + r, my + r], fill="#ffffff",
                  outline="#222", width=2)
        d.text((mx, my), e["marker"], font=f_mark, fill="#111", anchor="mm")

    d.text((M, 10), "AREA MAP + filming property (dashed) — code-rendered"
           " base", font=font(22, True), fill="#444")
    img.save(out_path)
    print(f"map base -> {out_path}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", default="all",
                    choices=["all", "base", "sketch", "pure", "html"])
    args = ap.parse_args()
    if args.only == "pure":
        run_pure()
        F.runlog({"kind": "stage", "stage": "s23_map_fp", "done": "pure"})
        return
    layout, rel = load_solved()
    if args.only in ("all", "base"):
        base = OUTB / BASE
        if base.exists():
            base.rename(OUTB / "bd_map_base_prev.png")
        render_map(layout, base)
        build_page()
    if args.only in ("all", "sketch"):
        p = sketch_prompt(layout, rel)
        try:
            prompts = F.load_plan("spatial_prompts")
        except Exception:
            prompts = {}
        prompts[SK_GPT] = p
        prompts[SK_NB2] = p
        F.save_plan("spatial_prompts", prompts)
        build_page()
        F.img_gpt("s23_map_fp_gpt", p, refs=[OUTB / BASE],
                  out_path=OUTB / SK_GPT)
        build_page()
        F.img_nb2("s23_map_fp_nb2", p,
                  [("NEIGHBOURHOOD MAP with the filming property inside the"
                    " dashed boundary — redraw ONLY the property as a"
                    " floor-plan sketch per the prompt; keep the map part"
                    " unchanged.", OUTB / BASE)],
                  out_path=OUTB / SK_NB2)
        build_page()
    if args.only == "html":
        build_page()
    F.runlog({"kind": "stage", "stage": "s23_map_fp", "done": args.only})


def build_page():
    def _esc(t):
        return _html.escape(str(t or ""))

    def cell(fn, cap):
        f = OUTB / fn
        if not f.exists():
            return (f"<div><div class='pending'>생성 대기/중…</div>"
                    f"<p class='cap'>{_esc(cap)}</p></div>")
        import datetime
        ts = datetime.datetime.fromtimestamp(f.stat().st_mtime)
        return (f"<div><img src='out/blockset/{fn}?v={int(ts.timestamp())}'>"
                f"<p class='cap'>{_esc(cap)} ({ts:%H:%M:%S})</p></div>")

    try:
        prom = F.load_plan("spatial_prompts")
    except Exception:
        prom = {}

    def pdet(fn, title):
        p = prom.get(fn)
        return (f"<details><summary>{_esc(title)} 프롬프트 (실전송)"
                f"</summary><pre>{_esc(p)}</pre></details>") if p else ""

    def thumb(fn, cap):
        f = OUTB / fn
        if not f.exists():
            return f"<span class='mini'>{_esc(cap)}: 대기</span>"
        return (f"<div class='th'><img src='out/blockset/{fn}'>"
                f"<p class='mini'>{_esc(cap)}</p></div>")

    def lane(eng, map_fn, fp_fn):
        return f"""
<h2>lane {eng} — 과정</h2>
<div class='proc'>
  <div class='step'>
    <p class='stept'>STEP 1 — 무참조 T2I 지도 ({eng})
      <span class='noref'>참조 없음(프롬프트만)</span></p>
    {cell(map_fn, '지도 (지도 문법 계약 + v12 미니멀 구조 줄 + 파선 경계)')}
    {pdet(map_fn, 'STEP 1 지도')}
  </div>
  <div class='arrow'>→ i2i</div>
  <div class='step'>
    <p class='stept'>STEP 2 — 촬영지(파선 안)만 fp 상세화 ({eng})</p>
    <div class='refrow'>{thumb(map_fn, f'참조 = STEP 1 지도 ({eng})')}</div>
    {cell(fp_fn, '촬영지만 상세 fp — 지도 부분 원본 유지, 정체 주입+실외 봉인')}
    {pdet(fp_fn, 'STEP 2 상세화')}
  </div>
</div>
"""

    PAGE.write_text(f"""<!doctype html><meta charset='utf-8'>
<title>s23 — 무참조 지도 → 촬영지만 fp 상세화</title>
<style>
body{{font-family:system-ui,'Apple SD Gothic Neo',sans-serif;margin:24px;
background:#fafafa;color:#222;max-width:1560px}}
h1{{font-size:20px}} h2{{font-size:16px;margin-top:28px;border-bottom:2px
solid #ddd;padding-bottom:4px}}
img{{width:100%;display:block;background:#fff;border:1px solid #ddd;
padding:6px;box-sizing:border-box}}
.cap{{font-size:12px;color:#333;margin:4px 0 12px;font-weight:600}}
.grid{{display:grid;grid-template-columns:1fr 1fr;gap:14px}}
.proc{{display:grid;grid-template-columns:1fr 46px 1fr;gap:10px;
align-items:start}}
.arrow{{font-size:14px;font-weight:700;color:#666;text-align:center;
padding-top:180px}}
.stept{{font-size:13px;font-weight:700;color:#234}}
.noref{{background:#234;color:#fff;font-size:11px;padding:2px 8px;
border-radius:4px;margin-left:6px}}
.refrow{{display:flex;gap:8px;margin:4px 0}}
.th{{width:130px}} .th img{{padding:2px}}
.mini{{font-size:10.5px;color:#667;margin:2px 0}}
.note{{background:#fff8e6;border:1px solid #eeddaa;padding:10px 14px;
font-size:13px}}
.old{{opacity:.75;border:2px dashed #bbb;padding:10px 14px;margin-top:40px}}
.pending{{padding:60px 10px;text-align:center;color:#999;background:#fff;
border:1px dashed #ccc}}
pre{{white-space:pre-wrap;font-size:11px;background:#fff;border:1px solid
#e5e5e5;padding:10px}}
details{{margin:8px 0}} summary{{font-size:12px;color:#557;cursor:pointer}}
</style>
<h1>s23 — ① 아무 참조 없이 그 지역을 "지도처럼" T2I → ② 그 지도 위에
i2i 로 촬영지(옥탑방 부지)만 상세 fp 화</h1>
<div class='note'>사용자 정정 반영: 코드 렌더 참조 X — <b>STEP 1 은 완전
무참조 T2I</b>(지도 문법 계약+v12 미니멀 구조 줄+촬영지 파선 경계),
<b>STEP 2 는 STEP 1 지도만 참조</b>해 파선 안 촬영지만 fp 상세화(정체
주입+실외 봉인, fp 어휘=S15.STYLE_2D). 엔진별 lane 독립(gpt→gpt /
nb2→nb2). 각 단계의 참조 이미지(작게)와 실전송 프롬프트 병기.</div>
{lane('gpt', PURE_MAP_GPT, PURE_FP_GPT)}
{lane('nb2', PURE_MAP_NB2, PURE_FP_NB2)}
<div class='old'>
<h2 style='margin-top:0'>(참고 — 사용자 정정으로 대체된 1차 시도: 코드
렌더 베이스를 참조로 사용)</h2>
<div class='grid'>
{cell(BASE, '코드 렌더 지도 베이스 (s22 좌표)')}
<div>{cell(SK_GPT, 'gpt edit — 참조: 코드 베이스')}
{cell(SK_NB2, 'nb2 i2i — 참조: 코드 베이스')}</div>
</div>
{pdet(SK_GPT, '1차 시도 스케치')}
</div>
""", encoding="utf-8")
    print(f"page -> {PAGE}")


if __name__ == "__main__":
    main()
