/* ============================================================
   mode-a.jsx — A 타입: 작업된 정방형 배너 1장 → 4종 사이즈 확장
   ============================================================ */

const { useState, useEffect, useRef, useCallback } = React;

/* Tweak defaults — host can rewrite this block on disk */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "variation": "natural",
  "canvasScale": 0.65,
  "showZones": false,
  "showAnchors": true,
  "showBbox": false,
  "autoGenerate": true,
  "grainEnabled": true,
  "apiEnabled": false,
  "apiEndpoint": "",
  "apiModel": "gemini",
  "pipelineMode": "outpaint",
  "safePadding": 0,
  "fitMode": "contain"
}/*EDITMODE-END*/;

/* API key kept in localStorage only (never in source file) */
const API_KEY_STORAGE = "banner-forge-gemini-key";

const SPECS = window.BannerGen.BANNER_SPECS;

/* Compute a snapshot string representing the inputs that affect one banner's
   AI output. If snapshot equals banner.lastSpec → no regen needed. */
function computeSpecSnapshot(shotSpec, variation, bannerId) {
  if (!shotSpec) return null;
  const shot = shotSpec.shots ? shotSpec.shots.find(s => s.banner === bannerId) : null;
  return JSON.stringify({
    shared: shotSpec.shared || null,
    shot: shot || null,
    variation,
  });
}

function ModeAApp({ onBackToEntry }) {
  /* ---- State ---- */
  const [tweaks, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const [source, setSource] = useState(null);     // {file, dataUrl, img, dim}
  const [shotSpec, setShotSpec] = useState(null); // full shot_spec object
  const [specLoading, setSpecLoading] = useState(false);
  const [banners, setBanners] = useState(() =>
    SPECS.map(s => ({
      id: s.id,
      status: "idle",
      progress: 0,
      dataUrl: null,
      blob: null,
      anchor: { ...s.anchor },
      scale: 1.0,
      lastSource: null, // 'ai' | 'local' | 'smart-crop'
    }))
  );
  const [selectedId, setSelectedId] = useState(null);
  const [tab, setTab] = useState("source"); // source | spec | banners | api
  const [exporting, setExporting] = useState(false);
  const [apiKey, setApiKey] = useState(() => localStorage.getItem(API_KEY_STORAGE) || "");
  const persistApiKey = useCallback((v) => {
    setApiKey(v);
    if (v) localStorage.setItem(API_KEY_STORAGE, v);
    else   localStorage.removeItem(API_KEY_STORAGE);
  }, []);

  /* ---- Source upload ---- */
  const handleFile = useCallback(async (file) => {
    const rawDataUrl = await new Promise((res, rej) => {
      const r = new FileReader();
      r.onload = () => res(r.result);
      r.onerror = rej;
      r.readAsDataURL(file);
    });
    const rawImg = await new Promise((res, rej) => {
      const i = new Image();
      i.onload = () => res(i);
      i.onerror = rej;
      i.src = rawDataUrl;
    });
    // ----------------------------------------------------------
    // 투명 여백 자동 크롭 (alpha bbox → 1:1 square)
    //   PNG 가 콘텐츠 주변에 투명 패딩을 포함한 경우, 가시 영역만 남기고
    //   1:1 정방형으로 다듬어서 .src-frame (cover) 에 꽉 차게 보이도록.
    //   불투명 픽셀이 거의 없거나 이미 꽉 차 있으면 원본 그대로 사용.
    // ----------------------------------------------------------
    const { dataUrl, img } = await (async () => {
      try {
        const W = rawImg.naturalWidth, H = rawImg.naturalHeight;
        const c0 = document.createElement("canvas");
        c0.width = W; c0.height = H;
        const ctx0 = c0.getContext("2d");
        ctx0.drawImage(rawImg, 0, 0);
        const d = ctx0.getImageData(0, 0, W, H).data;
        let x0 = W, y0 = H, x1 = -1, y1 = -1;
        for (let y = 0; y < H; y++) {
          for (let x = 0; x < W; x++) {
            if (d[(y * W + x) * 4 + 3] > 8) {
              if (x < x0) x0 = x;
              if (x > x1) x1 = x;
              if (y < y0) y0 = y;
              if (y > y1) y1 = y;
            }
          }
        }
        // 불투명 픽셀 없음 → 원본 사용
        if (x1 < 0) return { dataUrl: rawDataUrl, img: rawImg };
        const bw = x1 - x0 + 1, bh = y1 - y0 + 1;
        // 이미 꽉 차 있으면 원본 사용 (양쪽 합쳐 2% 미만 여백)
        if (bw / W > 0.98 && bh / H > 0.98) {
          return { dataUrl: rawDataUrl, img: rawImg };
        }
        // bbox 를 둘러싸는 정사각형 (center-pad)
        const size = Math.max(bw, bh);
        const cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
        let sx = Math.round(cx - size / 2);
        let sy = Math.round(cy - size / 2);
        // canvas 경계 안으로 clamp
        if (sx < 0) sx = 0;
        if (sy < 0) sy = 0;
        if (sx + size > W) sx = W - size;
        if (sy + size > H) sy = H - size;
        const out = document.createElement("canvas");
        out.width = size; out.height = size;
        const octx = out.getContext("2d");
        octx.drawImage(c0, sx, sy, size, size, 0, 0, size, size);
        const outUrl = out.toDataURL("image/png");
        const outImg = await new Promise((res, rej) => {
          const i = new Image();
          i.onload = () => res(i);
          i.onerror = rej;
          i.src = outUrl;
        });
        return { dataUrl: outUrl, img: outImg };
      } catch (e) {
        console.warn("alpha-crop failed, using original", e);
        return { dataUrl: rawDataUrl, img: rawImg };
      }
    })();
    setSource({
      file,
      blob: file,
      dataUrl,
      img,
      name: file.name,
      dim: { w: img.naturalWidth, h: img.naturalHeight },
    });
    // Stash for pipeline.html debug view (best-effort — large dataUrls may overflow localStorage)
    try { localStorage.setItem("bannerforge_last_source", dataUrl); } catch (e) { /* ignore quota */ }
    // Reset banners
    setBanners(SPECS.map(s => ({
      id: s.id,
      status: "idle",
      progress: 0,
      dataUrl: null,
      blob: null,
      anchor: { ...s.anchor },
      scale: 1.0,
      lastSource: null,
    })));
    setShotSpec(null);
    setTab("spec");
  }, []);

  /* ---- Auto-generate shot_spec after upload ---- */
  useEffect(() => {
    if (!source || shotSpec || !tweaks.autoGenerate) return;
    let cancelled = false;
    setSpecLoading(true);
    window.BannerGen.generateShotSpec(source.dataUrl).then(s => {
      if (cancelled) return;
      setShotSpec(s);
      setSpecLoading(false);
    });
    return () => { cancelled = true; };
  }, [source, shotSpec, tweaks.autoGenerate]);

  /* ---- Generate a single banner ---- */
  const generateOne = useCallback(async (specId, opts = {}) => {
    if (!source) return;
    const spec = SPECS.find(s => s.id === specId);
    const current = banners.find(b => b.id === specId);

    // Update to "gen" state with progress
    setBanners(prev => prev.map(b =>
      b.id === specId ? { ...b, status: "gen", progress: 0, dataUrl: null } : b
    ));

    try {
      // -------- HYBRID PIPELINE (default) --------
      // master + smart-crop → AI 호출 0회. source 이미지가 곧 master.
      if (tweaks.pipelineMode === "hybrid") {
        const { canvas, source: gSource, crop } =
          await window.BannerGen.generateBannerFromMaster(source.img, spec, {
            grain: tweaks.grainEnabled,
            onProgress: (p) => {
              setBanners(prev => prev.map(b =>
                b.id === specId ? { ...b, progress: p } : b
              ));
            },
          });
        const blob = await window.BannerGen.canvasToBlob(canvas);
        const dataUrl = canvas.toDataURL("image/png");
        const snapshot = computeSpecSnapshot(shotSpec, "hybrid:" + spec.id, specId);
        setBanners(prev => prev.map(b =>
          b.id === specId
            ? { ...b, status: "done", progress: 100, dataUrl, blob, lastSource: gSource, lastSpec: snapshot, lastCrop: crop }
            : b
        ));
        return;
      }

      // -------- LEGACY: per-size AI outpaint --------
      const apiConfig = {
        enabled: tweaks.apiEnabled,
        endpoint: tweaks.apiEndpoint,
      };
      // 이전 결과가 있으면 재호출 시 그 결과를 base 로 → AI 가 placement 다시
      // 안 하고 spec 변경분만 반영. 첫 호출(=current.dataUrl 없음)일 때만 source 사용.
      const inputDataUrl = current && current.dataUrl ? current.dataUrl : null;
      const { canvas, source: gSource } = await window.BannerGen.generateBanner(
        source.img, source.dataUrl, spec, shotSpec,
        {
          apiConfig,
          variation: opts.variation || tweaks.variation,
          anchor: opts.anchor || current.anchor,
          scale: opts.scale != null ? opts.scale : current.scale,
          productMargin: 1 - (tweaks.safePadding || 15) / 100,
          fitMode: "contain", // aspect-preserving; cover 비활성
          inputDataUrl,
          grain: tweaks.grainEnabled,
          onProgress: (p) => {
            setBanners(prev => prev.map(b =>
              b.id === specId ? { ...b, progress: p } : b
            ));
          },
        }
      );
      const blob = await window.BannerGen.canvasToBlob(canvas);
      const dataUrl = canvas.toDataURL("image/png");
      const snapshot = computeSpecSnapshot(shotSpec, opts.variation || tweaks.variation, specId);
      setBanners(prev => prev.map(b =>
        b.id === specId
          ? { ...b, status: "done", progress: 100, dataUrl, blob, lastSource: gSource, lastSpec: snapshot }
          : b
      ));
    } catch (e) {
      console.error("generateBanner failed", e);
      setBanners(prev => prev.map(b =>
        b.id === specId ? {
          ...b,
          status: "err",
          progress: 0,
          errorMsg: e.message || "AI 호출 실패",
        } : b
      ));
    }
  }, [source, banners, tweaks.variation, tweaks.apiEnabled, tweaks.apiEndpoint, shotSpec]);

  /* ---- Sequential generation helper ----
     Generates banners one-by-one with a delay between each call to
     avoid Gemini's per-minute rate limits.
     ---------------------------------------------------------------- */
  const sequentialGenRef = useRef(false);
  const generateAllSequentially = useCallback(async (specs = SPECS) => {
    if (sequentialGenRef.current) return; // already running
    sequentialGenRef.current = true;
    // Hybrid mode runs everything locally — no rate-limit delay needed.
    const interCallDelay = tweaks.pipelineMode === "hybrid" ? 0 : 12000;
    try {
      for (let i = 0; i < specs.length; i++) {
        const s = specs[i];
        await generateOne(s.id);
        if (i < specs.length - 1 && interCallDelay > 0) {
          await new Promise(r => setTimeout(r, interCallDelay));
        }
      }
    } finally {
      sequentialGenRef.current = false;
    }
  }, [generateOne, tweaks.pipelineMode]);

  /* ---- Generate only banners whose spec changed since last successful gen ---- */
  const generateChangedOnly = useCallback(async () => {
    if (sequentialGenRef.current) return;
    const changed = SPECS.filter(s => {
      const b = banners.find(x => x.id === s.id);
      // Never generated yet (idle or err) → include
      if (!b || b.status !== "done") return true;
      const snap = computeSpecSnapshot(shotSpec, tweaks.variation, s.id);
      return snap !== b.lastSpec;
    });
    if (changed.length === 0) return;
    await generateAllSequentially(changed);
  }, [banners, shotSpec, tweaks.variation, generateAllSequentially]);

  /* ---- Generate ONE NEXT banner (testing-friendly: pause between calls) ----
     사용자가 선택한 배너(selectedId) 가 있고 그게 pending 이면 그 배너부터.
     없으면 SPECS 순서대로 첫 pending 배너. → 사용자가 클릭으로 우선순위 결정 가능.
     -------------------------------------------------------------------- */
  const generateNextOne = useCallback(async () => {
    if (sequentialGenRef.current) return;

    const isPending = (b, specId) => {
      if (!b || b.status === "gen") return false;
      if (b.status !== "done") return true;
      const snap = computeSpecSnapshot(shotSpec, tweaks.variation, specId);
      return snap !== b.lastSpec;
    };

    // 1) 선택된 배너가 pending 이면 우선
    if (selectedId) {
      const sel = banners.find(x => x.id === selectedId);
      if (isPending(sel, selectedId)) {
        await generateOne(selectedId);
        return;
      }
    }
    // 2) 아니면 SPECS 순서대로 첫 pending
    const pending = SPECS.find(s => isPending(banners.find(x => x.id === s.id), s.id));
    if (!pending) return;
    await generateOne(pending.id);
  }, [banners, shotSpec, tweaks.variation, generateOne, selectedId]);

  /* ---- Auto-generate all banners once shotSpec is ready ---- */
  useEffect(() => {
    if (!source || !shotSpec || !tweaks.autoGenerate) return;
    // Only kick off if none are generated yet
    if (banners.some(b => b.status !== "idle")) return;
    // In outpaint mode, do NOT auto-fire — AI costs money, user must click.
    if (tweaks.pipelineMode === "outpaint") return;
    generateAllSequentially();
    setTab("banners");
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [source, shotSpec]);

  /* ---- Regenerate when variation changes (only after initial) ---- */
  const prevVariation = useRef(tweaks.variation);
  useEffect(() => {
    if (prevVariation.current !== tweaks.variation) {
      prevVariation.current = tweaks.variation;
      // Do NOT auto-batch on variation change in outpaint mode — burns credits.
      // User can click "다음 배너 생성" to do it one at a time.
      if (tweaks.pipelineMode !== "outpaint" && source && banners.some(b => b.status === "done")) {
        generateAllSequentially();
      }
    }
  }, [tweaks.variation, source, banners, generateAllSequentially, tweaks.pipelineMode]);

  /* ---- Anchor change → live regenerate ----
     In outpaint mode this is too costly to fire on every drag, so we only
     update the anchor state and the user must click "다음 배너 생성" or the
     per-banner Regen button to actually fire the call. */
  const updateAnchor = useCallback((id, anchor) => {
    setBanners(prev => prev.map(b => b.id === id ? { ...b, anchor } : b));
  }, []);

  // Debounced regen on anchor change (only in non-outpaint modes)
  const anchorDebounce = useRef({});
  const onAnchorChange = useCallback((id, anchor) => {
    updateAnchor(id, anchor);
    if (tweaks.pipelineMode === "outpaint") return; // anchor changes don't auto-fire AI
    if (anchorDebounce.current[id]) clearTimeout(anchorDebounce.current[id]);
    anchorDebounce.current[id] = setTimeout(() => {
      generateOne(id, { anchor });
    }, 400);
  }, [updateAnchor, generateOne, tweaks.pipelineMode]);

  /* ---- Export ZIP ---- */
  const handleExport = useCallback(async () => {
    if (!banners.every(b => b.blob)) return;
    setExporting(true);
    try {
      const zip = new JSZip();
      const folder = zip.folder("banner-forge-export");
      const scale = window.BannerGen.RENDER_SCALE || 1;
      banners.forEach((b, i) => {
        const spec = SPECS[i];
        folder.file(`${spec.id}_${spec.w}x${spec.h}@${scale}x.png`, b.blob);
      });
      if (shotSpec) {
        folder.file("shot_spec.json", JSON.stringify(shotSpec, null, 2));
      }
      if (source) {
        // include the source for reference
        folder.file(`source_${source.name}`, source.file);
      }
      const blob = await zip.generateAsync({ type: "blob" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
      a.download = `banner-export-${stamp}.zip`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
    } finally {
      setExporting(false);
    }
  }, [banners, shotSpec, source]);

  /* ---- Derived ---- */
  const doneCount = banners.filter(b => b.status === "done").length;
  const genCount  = banners.filter(b => b.status === "gen").length;
  const errCount  = banners.filter(b => b.status === "err").length;
  const totalCount = banners.length;
  // Banners whose current spec differs from the snapshot used for last successful gen.
  // Idle / error banners are also considered "changed" (they need a first gen).
  const changedIds = banners
    .filter(b => {
      if (b.status !== "done") return true;
      const snap = computeSpecSnapshot(shotSpec, tweaks.variation, b.id);
      return snap !== b.lastSpec;
    })
    .map(b => b.id);
  const changedCount = changedIds.length;

  /* ---- Regenerate helper used by the status-bar primary button.
     In outpaint mode we ONLY run one at a time (cost control).
     In crop-only mode we batch (it's local, free).
     -------------------------------------------------------------------- */
  const regenerateAll = useCallback(() => {
    if (!source) return;
    if (tweaks.pipelineMode === "outpaint") {
      generateNextOne();
    } else {
      generateAllSequentially();
    }
  }, [source, generateAllSequentially, generateNextOne, tweaks.pipelineMode]);

  /* ---- Render sidebar ---- */
  const renderSidebar = () => {
    return (
      <div className="sidebar">
        <div className="sidebar-tabs">
          <button className={"tab " + (tab === "source" ? "active" : "")} onClick={() => setTab("source")}>Source</button>
          <button className={"tab " + (tab === "spec" ? "active" : "")} onClick={() => setTab("spec")}>Spec</button>
          <button className={"tab " + (tab === "banners" ? "active" : "")} onClick={() => setTab("banners")}>Banners</button>
          <button className={"tab " + (tab === "api" ? "active" : "")} onClick={() => setTab("api")}>
            API {tweaks.apiEnabled ? <span style={{color: "var(--success)", marginLeft: 2}}>●</span> : ""}
          </button>
        </div>
        <div className="sidebar-body">
          {tab === "source" && (
            <div className="fadein">
              <div className="section">
                <div className="section-head">
                  <span className={"num " + (source ? "done" : "active")}>1</span>
                  원본 배너
                </div>
                {!source && <Dropzone onFile={handleFile} />}
                {source && (
                  <SourceCard
                    src={source.dataUrl}
                    name={source.name}
                    dim={source.dim}
                    onReplace={() => {
                      const i = document.createElement("input");
                      i.type = "file"; i.accept = "image/*";
                      i.onchange = e => handleFile(e.target.files[0]);
                      i.click();
                    }}
                    onRemove={() => { setSource(null); setShotSpec(null); }}
                  />
                )}
              </div>
              {source && (
                <div className="section fadein">
                  <div className="section-head">
                    <span className={"num " + (shotSpec ? "done" : "active")}>2</span>
                    Shot Spec
                  </div>
                  <div style={{fontSize: 11, color: "var(--text-2)", lineHeight: 1.55, marginBottom: 8}}>
                    {specLoading ? "AI가 원본 이미지를 분석하고 배치 지시서를 작성 중…" :
                      shotSpec ? "지시서 생성 완료. Spec 탭에서 확인/편집할 수 있어요." :
                      "Spec 탭에서 자동 생성하거나 수동 입력하세요."}
                  </div>
                  <button
                    className="btn full"
                    onClick={() => setTab("spec")}
                  >
                    <Icon name="spec" /> Spec 탭으로 이동
                  </button>
                </div>
              )}

              {source && shotSpec && (
                <div className="section fadein">
                  <div className="section-head">
                    <span className={"num " + (doneCount === totalCount ? "done" : doneCount > 0 || genCount > 0 ? "active" : "")}>3</span>
                    배너 생성 & 다운로드
                  </div>
                  <div style={{fontSize: 11, color: "var(--text-2)", lineHeight: 1.55, marginBottom: 8}}>
                    {doneCount === totalCount
                      ? "모두 준비됐어요. 상단 Export ZIP으로 일괄 다운로드하세요."
                      : `${doneCount}/${totalCount}개 완료${genCount > 0 ? ` (${genCount}개 생성 중)` : ""}`}
                  </div>
                  <button
                    className="btn primary full"
                    disabled={doneCount !== totalCount || exporting}
                    onClick={handleExport}
                  >
                    <Icon name="download" />
                    {exporting ? "Packing…" : "Export ZIP (모두)"}
                  </button>
                </div>
              )}
            </div>
          )}

          {tab === "spec" && (
            <div className="fadein">
              {!source && (
                <div style={{
                  padding: 14, background: "var(--bg-2)",
                  borderRadius: 6, color: "var(--text-2)", fontSize: 12, lineHeight: 1.6,
                }}>
                  먼저 Source 탭에서 원본 배너를 업로드하세요.
                </div>
              )}
              {source && specLoading && (
                <div className="section">
                  <div className="section-head"><span className="num active">·</span> AI 분석 중</div>
                  <div className="spec-block">
                    <div className="skel" style={{position:"relative", height: 12, borderRadius: 3, marginBottom: 8}} />
                    <div className="skel" style={{position:"relative", height: 60, borderRadius: 3}} />
                  </div>
                </div>
              )}
              {source && shotSpec && (
                <>
                  <div className="section">
                    <div className="section-head">
                      <span className="num done">·</span>
                      Shared
                    </div>
                    <div className="field">
                      <div className="field-label">Mood</div>
                      <input
                        className="input"
                        value={shotSpec.shared.mood}
                        onChange={e => setShotSpec({
                          ...shotSpec,
                          shared: { ...shotSpec.shared, mood: e.target.value }
                        })}
                      />
                    </div>
                    <div className="field">
                      <div className="field-label">Source lighting note</div>
                      <textarea
                        className="textarea"
                        value={shotSpec.shared.source_lighting_note}
                        onChange={e => setShotSpec({
                          ...shotSpec,
                          shared: { ...shotSpec.shared, source_lighting_note: e.target.value }
                        })}
                        rows={3}
                      />
                    </div>
                    <div className="field">
                      <div className="field-label" style={{ display: "flex", alignItems: "center", gap: 6 }}>
                        <Icon name="bolt" size={11} />
                        <span>공통 직접 지시 (모든 배너에 적용)</span>
                      </div>
                      <textarea
                        className="textarea"
                        value={shotSpec.shared.user_directive || ""}
                        placeholder={`4개 배너 모두에 적용할 지시를 자연어로.
예) "전체적으로 미니멀 화이트 톤", "상품 그림자 더 진하게"`}
                        onChange={e => setShotSpec({
                          ...shotSpec,
                          shared: { ...shotSpec.shared, user_directive: e.target.value }
                        })}
                        rows={2}
                        style={{
                          fontSize: 12,
                          background: (shotSpec.shared.user_directive || "").trim()
                            ? "var(--accent-soft)" : undefined,
                        }}
                      />
                    </div>
                  </div>
                  <div className="section">
                    <div className="section-head">
                      <span className="num">·</span>
                      Per-banner shots
                    </div>
                    {shotSpec.shots.map((shot, i) => (
                      <SpecBlock
                        key={shot.banner}
                        spec={SPECS[i]}
                        shotSpec={shot}
                        onChange={updated => {
                          const next = { ...shotSpec };
                          next.shots = [...shotSpec.shots];
                          next.shots[i] = updated;
                          setShotSpec(next);
                        }}
                      />
                    ))}
                  </div>
                  <button
                    className={"btn full " + (changedCount > 0 ? "primary" : "")}
                    disabled={changedCount === 0 || genCount > 0}
                    onClick={() => tweaks.pipelineMode === "outpaint" ? generateNextOne() : generateChangedOnly()}
                    title={
                      changedCount === 0
                        ? "변경된 배너가 없어요"
                        : tweaks.pipelineMode === "outpaint"
                          ? "다음 1개 배너만 호출 (테스트용)"
                          : `변경된 ${changedCount}개 배너만 다시 생성합니다`
                    }
                  >
                    <Icon name="refresh" />
                    {changedCount === 0
                      ? "모두 최신 상태"
                      : tweaks.pipelineMode === "outpaint"
                        ? `다음 1개 생성 (${changedCount} 남음)`
                        : `변경된 ${changedCount}개만 다시 생성`}
                  </button>
                  <button
                    className="btn ghost full sm"
                    style={{ marginTop: 6, fontSize: 11 }}
                    disabled={genCount > 0}
                    onClick={() => {
                      if (!confirm(`4개 배너를 모두 다시 호출합니다.\nAI 크레딧이 ${totalCount}회 차감돼요. 진행할까요?`)) return;
                      generateAllSequentially();
                    }}
                  >
                    전체 강제 재생성 ({totalCount}회 호출)
                  </button>
                </>
              )}
            </div>
          )}

          {tab === "banners" && (
            <div className="fadein">
              {!source && (
                <div style={{
                  padding: 14, background: "var(--bg-2)",
                  borderRadius: 6, color: "var(--text-2)", fontSize: 12, lineHeight: 1.6,
                }}>
                  원본 배너를 먼저 업로드하세요.
                </div>
              )}
              {source && (
                <>
                  <div className="section">
                    <div className="section-head">
                      <span className={"num " + (doneCount === totalCount ? "done" : "active")}>·</span>
                      {SPECS.length} banners
                    </div>
                    {SPECS.map(s => {
                      const b = banners.find(x => x.id === s.id);
                      return (
                        <BannerRow
                          key={s.id}
                          spec={s}
                          banner={b}
                          selected={selectedId === s.id}
                          onSelect={() => setSelectedId(s.id)}
                          onRegen={() => generateOne(s.id)}
                        />
                      );
                    })}
                  </div>
                  {selectedId && (() => {
                    const sel = banners.find(b => b.id === selectedId);
                    const selSpec = SPECS.find(s => s.id === selectedId);
                    return (
                      <div className="section fadein">
                        <div className="section-head">
                          <span className="num">·</span>
                          Selected: {selSpec.name}
                        </div>
                        <div className="field">
                          <div className="field-label">
                            Product scale
                            <span style={{marginLeft: "auto", fontFamily: "var(--font-mono)", color: "var(--text-1)"}}>
                              {Math.round(sel.scale * 100)}%
                            </span>
                          </div>
                          <input
                            type="range"
                            min="40" max="160" step="5"
                            value={Math.round(sel.scale * 100)}
                            onChange={e => {
                              const s = Number(e.target.value) / 100;
                              setBanners(prev => prev.map(b =>
                                b.id === selectedId ? { ...b, scale: s } : b
                              ));
                              if (anchorDebounce.current[selectedId]) clearTimeout(anchorDebounce.current[selectedId]);
                              anchorDebounce.current[selectedId] = setTimeout(() => generateOne(selectedId, { scale: s }), 350);
                            }}
                            style={{width: "100%"}}
                          />
                        </div>
                        <button
                          className="btn full"
                          onClick={() => generateOne(selectedId)}
                        >
                          <Icon name="refresh" /> 이 배너만 재생성
                        </button>
                        <div style={{fontSize: 11, color: "var(--text-2)", marginTop: 8, lineHeight: 1.55}}>
                          캔버스에서 앵커(파란 원)를 드래그하면 제품 위치가 자동으로 재생성됩니다.
                          {sel.lastSource && (
                            <> · 마지막 생성: <span className="chip" style={{
                              background: sel.lastSource === "ai" ? "var(--accent-soft)" : "var(--bg-3)",
                              color: sel.lastSource === "ai" ? "var(--accent)" : "var(--text-2)",
                            }}>{sel.lastSource}</span></>
                          )}
                        </div>
                      </div>
                    );
                  })()}
                </>
              )}
            </div>
          )}

          {tab === "api" && (
            <div className="fadein">
              <div className="section">
                <div className="section-head">
                  <span className={"num " + (tweaks.apiEnabled ? "done" : "")}>·</span>
                  연결 설정
                </div>

                <div className="field">
                  <div className="field-label">Endpoint URL (Cloudflare Worker 주소)</div>
                  <input
                    className="input"
                    placeholder="https://banner-forge-proxy.YOUR-NAME.workers.dev"
                    value={tweaks.apiEndpoint}
                    onChange={e => setTweak("apiEndpoint", e.target.value)}
                  />
                  <div style={{display: "flex", gap: 6, marginTop: 6}}>
                    <button
                      className="btn sm"
                      onClick={() => setTweak("apiEndpoint", "")}
                    >지우기</button>
                    <button
                      className="btn sm"
                      onClick={async () => {
                        if (!tweaks.apiEndpoint) return alert("Endpoint를 먼저 입력하세요");
                        try {
                          const r = await fetch(tweaks.apiEndpoint, { method: "GET" });
                          const t = await r.text();
                          if (r.ok && t.toLowerCase().includes("alive")) {
                            alert(`✓ 연결 OK\n\n프록시가 정상 동작 중이에요.`);
                          } else {
                            alert(`⚠ 응답은 받았지만 예상과 달라요\n\nstatus: ${r.status}\n${t.slice(0, 200)}`);
                          }
                        } catch (e) {
                          alert(`✗ 연결 실패\n\n${e.message}\n\nCloudflare Worker가 배포(Deploy)되었는지, URL이 정확한지 확인하세요.`);
                        }
                      }}
                    >연결 테스트</button>
                  </div>
                </div>

                <div className="field">
                  <div className="field-label" style={{justifyContent: "space-between"}}>
                    <span>활성화</span>
                    <span style={{
                      fontSize: 10,
                      color: tweaks.apiEnabled ? "var(--success)" : "var(--text-3)",
                      fontFamily: "var(--font-mono)",
                    }}>
                      {tweaks.apiEnabled ? "● ON" : "○ OFF"}
                    </span>
                  </div>
                  <button
                    className={"btn full " + (tweaks.apiEnabled ? "primary" : "")}
                    onClick={() => setTweak("apiEnabled", !tweaks.apiEnabled)}
                  >
                    {tweaks.apiEnabled ? "✓ AI 사용 중" : "토글하여 활성화"}
                  </button>
                  <div style={{fontSize: 11, color: "var(--text-2)", marginTop: 6, lineHeight: 1.55}}>
                    OFF면 로컬 폴백(블러 확장)을 사용합니다. AI 호출이 실패해도 자동으로 로컬로 전환됩니다.
                  </div>
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
    );
  };

  /* ---- Render canvas ---- */
  const renderCanvas = () => {
    if (!source) {
      return <div className="canvas-body"><EmptyState /></div>;
    }
    return (
      <div className="canvas-body">

        {/* ---- STATUS BAR ---- */}
        <div className={"api-status-bar " + (tweaks.pipelineMode === "hybrid" ? "" : tweaks.apiEnabled ? "" : "off") + (errCount > 0 ? " err" : "")}>
          <div className="api-status-left">
            <span className={"status-dot " + (errCount > 0 ? "red" : (tweaks.pipelineMode === "hybrid" || tweaks.apiEnabled) ? "green" : "gray")} />
            <div className="api-status-text">
              {tweaks.pipelineMode === "hybrid" ? (
                errCount > 0 ? (
                  <>
                    <strong>크롭 실패 ({errCount}/{totalCount})</strong>
                    <span className="sub">상품 검출 실패 또는 캔버스 오류 — 콘솔에서 상세 확인</span>
                  </>
                ) : genCount > 0 ? (
                  <>
                    <strong>Smart Crop 생성 중…</strong>
                    <span className="sub">로컬 처리 · AI 호출 0회 · {doneCount + 1}/{totalCount}</span>
                  </>
                ) : doneCount === totalCount ? (
                  <>
                    <strong>완료 · Smart Crop</strong>
                    <span className="sub">{totalCount}개 배너 모두 master에서 crop 완료 (AI 호출 0회)</span>
                  </>
                ) : (
                  <>
                    <strong>Hybrid 모드 (master + crop)</strong>
                    <span className="sub">source 가 master · AI 호출 없이 4종 즉시 생성 가능</span>
                  </>
                )
              ) : tweaks.apiEnabled ? (
                errCount > 0 ? (
                  <>
                    <strong>AI 호출 실패 ({errCount}/{totalCount})</strong>
                    <span className="sub">잠시 후 다시 시도하거나 API 키/한도를 확인하세요.</span>
                  </>
                ) : genCount > 0 ? (
                  <>
                    <strong>Gemini AI 호출 중…</strong>
                    <span className="sub">1개씩 호출 중 · {doneCount + 1}/{totalCount} 진행 중 · 결과 확인 후 다음 진행</span>
                  </>
                ) : doneCount === totalCount ? (
                  <>
                    <strong>AI 호출 완료</strong>
                    <span className="sub">{totalCount}개 배너 모두 Gemini로 생성됨</span>
                  </>
                ) : (
                  <>
                    <strong>Safe-Area Outpaint (AI)</strong>
                    <span className="sub">Cloudflare Worker 연결 OK · 호출하기 버튼을 누르세요</span>
                  </>
                )
              ) : (
                <>
                  <strong style={{color: "var(--warning)"}}>⚠ AI 모드인데 비활성 상태</strong>
                  <span className="sub">API 탭에서 활성화하거나 Tweaks에서 "Crop only"로 전환하세요</span>
                </>
              )}
            </div>
          </div>
          <div className="api-status-right">
            {tweaks.pipelineMode === "outpaint" && !tweaks.apiEnabled && (
              <button className="btn sm" onClick={() => setTab("api")}>
                <Icon name="settings" size={12} /> API 탭으로
              </button>
            )}
            <button
              className="btn primary sm"
              disabled={genCount > 0 || !source || changedCount === 0}
              onClick={regenerateAll}
              title={
                changedCount === 0
                  ? "모든 배너가 최신 상태입니다"
                  : tweaks.pipelineMode === "outpaint"
                    ? `다음 1개 배너만 AI로 호출 (남은 ${changedCount}개)`
                    : tweaks.pipelineMode === "hybrid"
                      ? `${changedCount}개 배너 즉시 crop`
                      : `변경된 ${changedCount}개 배너를 AI로 호출`
              }
            >
              <Icon name="bolt" size={12} />
              {errCount > 0
                ? `다시 시도 (${errCount})`
                : changedCount === 0
                  ? "최신 상태"
                  : tweaks.pipelineMode === "outpaint"
                    ? (() => {
                        // Selected banner takes priority
                        const isPending = (b, s) => {
                          if (!b || b.status === "gen") return false;
                          if (b.status !== "done") return true;
                          return computeSpecSnapshot(shotSpec, tweaks.variation, s.id) !== b.lastSpec;
                        };
                        let next;
                        if (selectedId) {
                          const sel = SPECS.find(s => s.id === selectedId);
                          const selBanner = banners.find(x => x.id === selectedId);
                          if (sel && isPending(selBanner, sel)) next = sel;
                        }
                        if (!next) {
                          next = SPECS.find(s => isPending(banners.find(x => x.id === s.id), s));
                        }
                        if (!next) return "최신 상태";
                        const isSelected = next.id === selectedId;
                        const nextLabel = next.name.replace(/^.*?·\s*/, "");
                        return `${isSelected ? "★ " : ""}${nextLabel} 생성 (${changedCount} 남음)`;
                      })()
                    : tweaks.pipelineMode === "hybrid"
                      ? `${changedCount}개 즉시 생성`
                      : doneCount > 0
                        ? `변경된 ${changedCount}개 호출`
                        : "AI 호출하기"}
            </button>
          </div>
        </div>

        <div className="canvas-stage">
          {/* Source preview */}
          <div className="canvas-source">
            <div className="label">SOURCE · 1:1</div>
            <div
              className="src-frame"
              style={{ backgroundImage: `url(${source.dataUrl})` }}
            />
          </div>

          {/* Flow arrow */}
          <div className="flow-arrow">
            <div className="stem" />
            <div>OUTPAINTING</div>
            <div className="stem" />
            <div style={{color: "var(--text-2)"}}>▼</div>
          </div>

          {/* Banner grid at true aspect ratio */}
          <div className="banner-grid">
            {SPECS.map(spec => {
              const b = banners.find(x => x.id === spec.id);
              return (
                <BannerCard
                  key={spec.id}
                  spec={spec}
                  banner={b}
                  scale={tweaks.canvasScale}
                  selected={selectedId === spec.id}
                  onSelect={() => setSelectedId(spec.id)}
                  showZones={tweaks.showZones}
                  showAnchor={tweaks.showAnchors && selectedId === spec.id}
                  showBbox={tweaks.showBbox && tweaks.pipelineMode === "hybrid"}
                  onAnchorChange={a => onAnchorChange(spec.id, a)}
                  onRegen={() => generateOne(spec.id)}
                />
              );
            })}
          </div>
        </div>

        {/* Canvas toolbar */}
        <div className="canvas-toolbar">
          <div className="tool-group">
            <button
              className={"tool " + (tweaks.showZones ? "active" : "")}
              onClick={() => setTweak("showZones", !tweaks.showZones)}
              title="Show red zones (text/UI 영역)"
            >
              <Icon name={tweaks.showZones ? "eye" : "eyeoff"} size={12} />
              Zones
            </button>
            <button
              className={"tool " + (tweaks.showAnchors ? "active" : "")}
              onClick={() => setTweak("showAnchors", !tweaks.showAnchors)}
              title="Show anchor handles"
            >
              <Icon name="target" size={12} />
              Anchor
            </button>
          </div>
          <div className="tool-group">
            <button className="tool" onClick={() => setTweak("canvasScale", Math.max(0.3, tweaks.canvasScale - 0.1))}>
              <Icon name="zoom_out" size={12} />
            </button>
            <button className="tool" style={{minWidth: 50, fontFamily: "var(--font-mono)"}}>
              {Math.round(tweaks.canvasScale * 100)}%
            </button>
            <button className="tool" onClick={() => setTweak("canvasScale", Math.min(1.2, tweaks.canvasScale + 0.1))}>
              <Icon name="zoom_in" size={12} />
            </button>
          </div>
          <div style={{flex: 1}} />
          {genCount > 0 && (
            <div className="tool-group" style={{paddingLeft: 10, paddingRight: 10}}>
              <span style={{
                fontSize: 11, color: "var(--accent)",
                fontFamily: "var(--font-mono)",
                display: "inline-flex", alignItems: "center", gap: 6,
              }}>
                <span className="dot live" />
                {genCount}개 outpainting…
              </span>
            </div>
          )}
        </div>

        {/* Bottom progress strip (when generating) */}
        {(genCount > 0 || (source && doneCount < totalCount)) && doneCount < totalCount && (
          <window.DraggableStrip>
            <Icon name="bolt" size={14} />
            <span style={{fontFamily: "var(--font-mono)", fontSize: 11}}>
              {doneCount}/{totalCount}
            </span>
            <div className="bar"><span style={{width: `${(doneCount/totalCount)*100}%`}} /></div>
            <span style={{color: "var(--text-2)", fontSize: 11}}>
              모든 배너가 완료되면 자동으로 Export 활성화
            </span>
          </window.DraggableStrip>
        )}

        {doneCount === totalCount && totalCount > 0 && (
          <window.DraggableStrip className="fadein">
            <Icon name="check" size={14} style={{color: "var(--success)"}} />
            <span style={{fontSize: 12}}>{totalCount}개 배너 모두 준비 완료</span>
            <button className="btn primary sm" onClick={handleExport} disabled={exporting}>
              <Icon name="download" size={12} />
              {exporting ? "Packing…" : "Export ZIP"}
            </button>
          </window.DraggableStrip>
        )}
      </div>
    );
  };

  /* ---- Render Tweaks panel ---- */
  const renderTweaks = () => (
    <window.TweaksPanel title="Tweaks">
      <window.TweakSection label="Pipeline">
        <window.TweakRadio
          label="Mode"
          value={tweaks.pipelineMode}
          onChange={v => setTweak("pipelineMode", v)}
          options={[
            { value: "outpaint", label: "Safe-Area Outpaint (AI)" },
            { value: "hybrid",   label: "Crop only (no AI)" },
          ]}
        />
        <window.TweakSlider
          label="Safe area 여백"
          value={tweaks.safePadding}
          onChange={v => setTweak("safePadding", v)}
          min={0} max={30} step={1}
          unit="%"
        />
        <window.TweakToggle
          label="Show bbox overlay"
          value={tweaks.showBbox}
          onChange={v => setTweak("showBbox", v)}
        />
      </window.TweakSection>
      <window.TweakSection label="Variation">
        <window.TweakRadio
          label="Background"
          value={tweaks.variation}
          onChange={v => setTweak("variation", v)}
          options={[
            { value: "natural", label: "Natural" },
            { value: "studio",  label: "Studio" },
            { value: "vivid",   label: "Vivid" },
          ]}
        />
      </window.TweakSection>
      <window.TweakSection label="Canvas">
        <window.TweakSlider
          label="Preview scale"
          value={Math.round(tweaks.canvasScale * 100)}
          onChange={v => setTweak("canvasScale", v / 100)}
          min={30} max={120} step={5}
          unit="%"
        />
        <window.TweakToggle
          label="Show red zones"
          value={tweaks.showZones}
          onChange={v => setTweak("showZones", v)}
        />
        <window.TweakToggle
          label="Show anchor handles"
          value={tweaks.showAnchors}
          onChange={v => setTweak("showAnchors", v)}
        />
      </window.TweakSection>
      <window.TweakSection label="Workflow">
        <window.TweakToggle
          label="Auto-generate on upload"
          value={tweaks.autoGenerate}
          onChange={v => setTweak("autoGenerate", v)}
        />
        <window.TweakToggle
          label="Film grain (realism)"
          value={tweaks.grainEnabled}
          onChange={v => setTweak("grainEnabled", v)}
        />
      </window.TweakSection>
    </window.TweaksPanel>
  );

  return (
    <div className="app">
      <TopBar
        sourceName={source ? source.name : ""}
        banners={banners}
        onExport={handleExport}
        exporting={exporting}
        mode="a"
        onBackToEntry={onBackToEntry}
      />
      <div className="workspace">
        {renderSidebar()}
        <div className="canvas" data-screen-label="Main">
          {renderCanvas()}
        </div>
      </div>
      {renderTweaks()}
    </div>
  );
}

window.ModeAApp = ModeAApp;
