/* ============================================================
   canvas.jsx — 캔버스 렌더 조각들
   · APCanvas      : 인터랙티브 550 캔버스 (선택·드래그·리사이즈)
   · APMiniCanvas  : 후보 랭킹용 소형 미리보기 (비인터랙티브)
   · APResultCard  : 랭킹된 후보 1개를 크게 — 지표 + 조정
   ============================================================ */
const { useRef: useCR, useState: useCS, useEffect: useCE } = React;

// 550×550 캔버스 고정 상수 (가이드라인 오버레이용)
const SAFE_550 = { x: 50, y: 40, cw: 550, ch: 550 };
const logoAspect550 = (value) => {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? Math.max(0.25, Math.min(20, n)) : 1;
};
const LOGO_550_MAX_COUNT = 3;
const LOGO_550_MAX_ROW = 225;
const LOGO_550_GAP = 10;
const LOGO_550_RIGHT = 500; // 550 - 우측여백 50
const LOGO_550_MID = 275;   // 절반선 — 로고 행 왼쪽 끝이 넘지 못함
function logoOrient550(g) {
  if (g && (g.orient === 'h' || g.orient === 'v')) return g.orient;
  return logoAspect550(g && g.aspect) >= 1.6 ? 'h' : 'v';
}
function logoScale550(value) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? Math.max(0.25, Math.min(4, n)) : 1;
}
// Per-axis scale helpers — fall back to uniform scale for backward compat
function logoScaleW550(g) {
  const n = Number(g && g.scaleW);
  if (Number.isFinite(n) && n > 0) return Math.max(0.25, Math.min(4, n));
  return logoScale550(g && g.scale);
}
function logoScaleH550(g) {
  const n = Number(g && g.scaleH);
  if (Number.isFinite(n) && n > 0) return Math.max(0.25, Math.min(4, n));
  return logoScale550(g && g.scale);
}
/* logoRow550 — base layout (scale=1) + per-logo scaleW/scaleH.
   When heightFor is provided (E-type layout) per-logo scales are skipped (fixed heights).
   Returns items with { url, w, h, baseW, baseH, rightEdge } where rightEdge is the
   precomputed right anchor so rendering can be top-right anchored without reflowing. */
function logoRow550(logos, heightFor, maxRow) {
  if (!heightFor && window.LogoPlacement550) {
    return window.LogoPlacement550.layout(logos, { maxRow });
  }
  const usePerLogoScale = !heightFor; // only A/B/C/D right-aligned rows
  const arr = (logos || []).slice(0, LOGO_550_MAX_COUNT).map((g) => {
    const aspect = logoAspect550(g.aspect);
    const orient = logoOrient550(g);
    // Base height: always scale=1 for layout purposes
    const baseH = heightFor ? heightFor({ ...g, orient, aspect }) : (orient === 'h' ? 20 : 60);
    const baseW = baseH * aspect;
    return { url: g.url, baseH, baseW, orient, aspect };
  });
  if (!arr.length) return { items: [], rowH: 0, gap: LOGO_550_GAP, rowW: 0, k: 1, minX: LOGO_550_RIGHT };
  // Global midpoint clamp (PR 37) — computed on BASE sizes (scaleW=scaleH=1)
  const totalW = arr.reduce((s, l) => s + l.baseW, 0) + LOGO_550_GAP * (arr.length - 1);
  const limit = Number.isFinite(Number(maxRow)) ? Number(maxRow) : LOGO_550_MAX_ROW;
  const cap = Math.min(limit, LOGO_550_RIGHT - LOGO_550_MID);
  let k = totalW > cap ? cap / totalW : 1;
  let rowW = totalW * k;
  let minX = LOGO_550_RIGHT - rowW;
  if (minX < LOGO_550_MID && totalW > 0) {
    k = (LOGO_550_RIGHT - LOGO_550_MID) / totalW;
    rowW = totalW * k;
    minX = LOGO_550_RIGHT - rowW;
  }
  const gap = LOGO_550_GAP * k;
  // Pre-compute each logo's right edge from base layout (right-to-left)
  const rightEdges = new Array(arr.length);
  let xR = LOGO_550_RIGHT;
  for (let i = arr.length - 1; i >= 0; i--) {
    rightEdges[i] = xR;
    xR = xR - arr[i].baseW * k - gap;
  }
  // Apply per-logo scaleW/scaleH with top-right anchor + hard clamp
  const logoList = (logos || []).slice(0, LOGO_550_MAX_COUNT);
  const items = arr.map((l, i) => {
    const bw = l.baseW * k, bh = l.baseH * k;
    let w, h;
    if (usePerLogoScale) {
      const g = logoList[i] || {};
      w = bw * logoScaleW550(g);
      h = bh * logoScaleH550(g);
      // Clamp: left edge ≥ LOGO_550_MID, right edge ≤ LOGO_550_RIGHT
      const maxW = rightEdges[i] - LOGO_550_MID;
      if (w > maxW && maxW > 0) w = maxW;
      if (w < 1) w = 1;
      // Clamp height to canvas below top margin
      if (h > 510) h = 510;
      if (h < 1) h = 1;
    } else {
      w = bw; h = bh;
    }
    return { url: l.url, w, h, baseW: bw, baseH: bh, rightEdge: rightEdges[i] };
  });
  const rowH = Math.max.apply(null, items.map((l) => l.h));
  return { items, rowH, gap, rowW, k, minX };
}

// 여백 가이드라인 오버레이 — 회색 밴드 + 헤어라인 + 레이블 (pointer-events: none)
function SafeGuideOverlay({ sx, sy, cw, ch }) {
  const band = { position: 'absolute', background: 'rgba(0,0,0,0.14)', pointerEvents: 'none' };
  const lbl = { position: 'absolute', fontSize: 10.5, fontWeight: 700, fontFamily: 'monospace', color: 'rgba(78,76,219,0.85)', pointerEvents: 'none' };
  const safeW = cw - sx * 2, safeH = ch - sy * 2;
  return (
    <>
      {/* 4개 여백 밴드 */}
      <div style={{ ...band, left: 0, top: 0, width: cw, height: sy }} />
      <div style={{ ...band, left: 0, top: ch - sy, width: cw, height: sy }} />
      <div style={{ ...band, left: 0, top: sy, width: sx, height: safeH }} />
      <div style={{ ...band, left: cw - sx, top: sy, width: sx, height: safeH }} />
      {/* 세이프 영역 내곽 헤어라인 */}
      <div style={{ position: 'absolute', left: sx, top: sy, width: safeW, height: safeH, outline: '1px solid rgba(78,76,219,0.55)', pointerEvents: 'none' }} />
      {/* 수직 여백 레이블 (우측 밴드) */}
      <span style={{ ...lbl, right: Math.floor(sx / 2) - 8, top: Math.floor(ch / 2) - 7 }}>{sx}</span>
      {/* 수평 여백 레이블 (상단 밴드) */}
      <span style={{ ...lbl, left: Math.floor(cw / 2) - 8, top: Math.floor(sy / 2) - 7 }}>{sy}</span>
    </>
  );
}

function APStage({ rects, dispW, editable, selectedId, onSelect, onChange, faintSafe, bounds, scorer, onBegin, logos, deco, decoPos, onDecoPosChange, onDecoScale, decoSelected, onSelectDeco, noReflow = true, onDeleteItem, guideOn = true, selLogoIdx, onSelectLogo, imageAdjust, imageAdjustBySource, onSelectionChange }) {
  const scale = dispW / 550;
  const drag = useCR(null);
  const decoDrag = useCR(null);
  const decoResize = useCR(null);
  const groupDrag = useCR(null);
  const wrapRef = useCR(null);
  const deleteGuardRef = useCR({ key: null, at: 0 });
  const [menu, setMenu] = useCS(null); // {x,y,idx}
  const [resizeModeId, setResizeModeId] = useCS(null);
  // 다중 선택 상태 — 내부 로컬. 부모는 단일 selectedId만 관리.
  const [multiIds, _setMultiIds] = useCS([]);
  const multiIdsRef = useCR([]);

  // ref와 state를 동기화하는 setMultiIds 래퍼
  const setMultiIds = (v) => {
    const next = typeof v === 'function' ? v(multiIdsRef.current) : v;
    multiIdsRef.current = next;
    _setMultiIds(next);
    onSelectionChange && onSelectionChange(next.slice());
  };

  // 외부에서 selectedId가 null로 초기화되면 다중 선택도 해제
  useCE(() => { if (selectedId === null) { setMultiIds([]); setResizeModeId(null); } }, [selectedId]);

  // ── 단일 아이템 드래그 ──
  const onItemDown = (e, idx, mode) => {
    if (!editable || e.button === 2) return;
    e.stopPropagation(); e.preventDefault();
    setMenu(null);
    const r = rects[idx];
    const id = r.id;

    // Shift/Cmd/Ctrl+클릭 → 다중 선택 토글 (드래그 시작 안 함)
    if (e.shiftKey || e.metaKey || e.ctrlKey) {
      setMultiIds((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]);
      onSelect && onSelect(id);
      onSelectLogo && onSelectLogo(null);
      return;
    }

    // 이미 다중 선택된 아이템을 이동 모드로 클릭 → 그룹 이동
    const curIds = multiIdsRef.current;
    if (mode === 'move' && curIds.length >= 2 && curIds.includes(id)) {
      const selRects = rects.filter((r2) => curIds.includes(r2.id));
      onBegin && onBegin();
      groupDrag.current = { mode: 'move', startX: e.clientX, startY: e.clientY, origRects: selRects.map((r2) => ({ ...r2 })) };
      window.addEventListener('pointermove', onGroupMove);
      window.addEventListener('pointerup', onGroupUp);
      return;
    }

    // 단일 선택 + 드래그
    setMultiIds([id]);
    onSelect && onSelect(id);
    onSelectLogo && onSelectLogo(null);
    onBegin && onBegin();
    drag.current = { idx, mode, startX: e.clientX, startY: e.clientY, orig: { ...r } };
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
  };

  const onMove = (e) => {
    const dr = drag.current; if (!dr) return;
    const dx = (e.clientX - dr.startX) / scale;
    const dy = (e.clientY - dr.startY) / scale;
    const next = rects.map((r) => ({ ...r }));
    const t = next[dr.idx]; const o = dr.orig;
    if (dr.mode === 'move') { t.x = o.x + dx; t.y = o.y + dy; } else {
      const a = o.aspect || o.w / o.h; const c = dr.mode;
      let w = Math.max(30, o.w + (c === 'se' || c === 'ne' ? dx : -dx));
      const h = w / a;
      t.w = w; t.h = h;
      t.x = c === 'nw' || c === 'sw' ? o.x + (o.w - w) : o.x;
      t.y = c === 'nw' || c === 'ne' ? o.y + (o.h - h) : o.y;
    }
    if (drag.current) drag.current.last = next;
    onChange && onChange(next);
  };

  const onUp = () => {
    const dr = drag.current;
    if (dr && dr.last && editable && bounds && window.AP550) {
      const draggedIsModel = rects[dr.idx] && rects[dr.idx].role === 'model';
      if (noReflow || draggedIsModel) { onChange && onChange(dr.last); } else {
        const isMove = dr.mode === 'move';
        const movedId = rects[dr.idx] ? rects[dr.idx].id : null;
        const models = dr.last.filter((r) => r.role === 'model');
        const others = dr.last.filter((r) => r.role !== 'model');
        const out = isMove ?
          (window.AP550.reflowMove ? window.AP550.reflowMove(others, bounds, scorer, movedId) : others)
          : (window.AP550.reflow ? window.AP550.reflow(others, bounds, scorer, null) : others);
        onChange && onChange((out || others).concat(models));
      }
    }
    drag.current = null;
    window.removeEventListener('pointermove', onMove);
    window.removeEventListener('pointerup', onUp);
  };

  // ── 그룹 이동/리사이즈 ──
  const onGroupMove = (e) => {
    const gd = groupDrag.current; if (!gd) return;
    const dx = (e.clientX - gd.startX) / scale;
    const dy = (e.clientY - gd.startY) / scale;
    const next = rects.map((r) => ({ ...r }));
    if (gd.mode === 'move') {
      gd.origRects.forEach((orig) => {
        const t = next.find((r) => r.id === orig.id);
        if (t) { t.x = orig.x + dx; t.y = orig.y + dy; }
      });
    } else if (gd.mode === 'resize') {
      // 그룹 중심 기준 대각선 거리 비율로 균일 확대/축소
      const origDist = Math.sqrt(gd.hW * gd.hW + gd.hH * gd.hH);
      const newHW = gd.hW + dx, newHH = gd.hH + dy;
      const newDist = Math.sqrt(newHW * newHW + newHH * newHH);
      const sf = origDist > 0 ? Math.max(0.1, newDist / origDist) : 1;
      gd.origRects.forEach((orig) => {
        const t = next.find((r) => r.id === orig.id);
        if (t) {
          const oCx = orig.x + orig.w / 2;
          const oCy = orig.y + orig.h / 2;
          t.w = Math.max(20, orig.w * sf);
          t.h = Math.max(20, orig.h * sf);
          t.x = gd.cx + (oCx - gd.cx) * sf - t.w / 2;
          t.y = gd.cy + (oCy - gd.cy) * sf - t.h / 2;
        }
      });
    }
    if (groupDrag.current) groupDrag.current.last = next;
    onChange && onChange(next);
  };

  const onGroupUp = () => {
    const gd = groupDrag.current;
    if (gd && gd.last) onChange && onChange(gd.last);
    groupDrag.current = null;
    window.removeEventListener('pointermove', onGroupMove);
    window.removeEventListener('pointerup', onGroupUp);
  };

  // 그룹 리사이즈 핸들(se) 포인터다운
  const onGroupHandleDown = (e) => {
    if (!editable) return;
    e.stopPropagation(); e.preventDefault();
    const curIds = multiIdsRef.current;
    if (curIds.length < 2) return;
    const selRects = rects.filter((r) => curIds.includes(r.id));
    let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
    selRects.forEach((r) => { minX = Math.min(minX, r.x); minY = Math.min(minY, r.y); maxX = Math.max(maxX, r.x + r.w); maxY = Math.max(maxY, r.y + r.h); });
    onBegin && onBegin();
    groupDrag.current = {
      mode: 'resize',
      startX: e.clientX, startY: e.clientY,
      cx: (minX + maxX) / 2, cy: (minY + maxY) / 2,
      hW: (maxX - minX) / 2, hH: (maxY - minY) / 2,
      origRects: selRects.map((r) => ({ ...r })),
    };
    window.addEventListener('pointermove', onGroupMove);
    window.addEventListener('pointerup', onGroupUp);
  };

  const openMenu = (e, idx) => {
    if (!editable) return;
    e.preventDefault(); e.stopPropagation();
    onSelect && onSelect(rects[idx].id);
    setMenu({ x: e.clientX, y: e.clientY, idx });
  };
  const openMenuAtPoint = (e) => {
    if (!editable) return;
    e.preventDefault(); e.stopPropagation();
    const stage = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - stage.left) / scale;
    const y = (e.clientY - stage.top) / scale;
    const hit = rects
      .map((r, idx) => ({ r, idx }))
      .filter(({ r }) => x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h)
      .sort((a, b) => (b.r.z || 0) - (a.r.z || 0))[0];
    if (!hit) return;
    onSelect && onSelect(hit.r.id);
    setMenu({ x: e.clientX, y: e.clientY, idx: hit.idx });
  };

  const changeZ = (idx, dir) => {
    const next = rects.map((r) => ({ ...r }));
    const zs = next.map((r) => r.z);
    next[idx].z = dir === 'front' ? Math.max.apply(null, zs) + 1 : Math.min.apply(null, zs) - 1;
    onBegin && onBegin();
    onChange && onChange(next);
    setMenu(null);
  };

  useCE(() => {
    if (!menu) return;
    const close = () => setMenu(null);
    window.addEventListener('pointerdown', close);
    return () => window.removeEventListener('pointerdown', close);
  }, [menu]);

  // Esc → 다중 선택 해제
  useCE(() => {
    if (!editable) return;
    const onKey = (e) => {
      if (e.key === 'Escape') { setMultiIds([]); onSelect && onSelect(null); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [editable]);

  // 방향키 이동 (단일/다중 선택 모두 지원)
  useCE(() => {
    if (!editable || selectedId == null) return;
    const onKey = (e) => {
      if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].indexOf(e.key) < 0) return;
      e.preventDefault();
      onBegin && onBegin();
      const step = e.shiftKey ? 10 : 1;
      const next = rects.map((r) => ({ ...r }));
      const curIds = multiIdsRef.current;
      const targets = curIds.length >= 2 ? curIds : [selectedId];
      targets.forEach((tid) => {
        const t = next.find((r) => r.id === tid);
        if (!t) return;
        if (e.key === 'ArrowUp') t.y -= step;
        else if (e.key === 'ArrowDown') t.y += step;
        else if (e.key === 'ArrowLeft') t.x -= step;
        else if (e.key === 'ArrowRight') t.x += step;
      });
      let reflowed = next;
      if (!noReflow && curIds.length < 2) {
        const moved = next.find((r) => r.id === selectedId);
        if (moved && moved.role !== 'model' && window.AP550 && window.AP550.reflowMove && bounds) {
          const models = next.filter((r) => r.role === 'model'), others = next.filter((r) => r.role !== 'model');
          reflowed = window.AP550.reflowMove(others, bounds, scorer, selectedId).concat(models);
        }
      }
      onChange && onChange(reflowed);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [editable, selectedId, rects, bounds, onChange, scorer, onBegin, noReflow]);

  // 그룹 바운딩 박스 계산 (2개 이상 선택 시)
  const groupBbox = (() => {
    if (multiIds.length < 2) return null;
    let minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
    rects.forEach((r) => {
      if (multiIds.includes(r.id)) {
        minX = Math.min(minX, r.x); minY = Math.min(minY, r.y);
        maxX = Math.max(maxX, r.x + r.w); maxY = Math.max(maxY, r.y + r.h);
      }
    });
    return minX < 1e9 ? { x: minX, y: minY, w: maxX - minX, h: maxY - minY } : null;
  })();

  const selectedTargetIds = multiIds.length >= 2 ? multiIds : (selectedId != null ? [selectedId] : []);
  const alignSelection = (axis, pos) => (e) => {
    if (!editable || !selectedTargetIds.length) return;
    e.preventDefault(); e.stopPropagation();
    const set = new Set(selectedTargetIds);
    const targets = rects.filter((r) => set.has(r.id));
    if (!targets.length) return;
    let min = Infinity, max = -Infinity;
    targets.forEach((r) => {
      const a = axis === 'x' ? r.x : r.y;
      const b = a + (axis === 'x' ? r.w : r.h);
      min = Math.min(min, a);
      max = Math.max(max, b);
    });
    const zone = bounds && Number.isFinite(bounds.x) && Number.isFinite(bounds.y) && Number.isFinite(bounds.w) && Number.isFinite(bounds.h)
      ? bounds
      : { x: SAFE_550.x, y: SAFE_550.y, w: SAFE_550.cw - SAFE_550.x * 2, h: SAFE_550.ch - SAFE_550.y * 2 };
    const size = Math.max(1, max - min);
    const zoneStart = axis === 'x' ? zone.x : zone.y;
    const zoneSize = axis === 'x' ? zone.w : zone.h;
    const target = pos === 'start' ? zoneStart : pos === 'center' ? zoneStart + (zoneSize - size) / 2 : zoneStart + zoneSize - size;
    const d = target - min;
    onBegin && onBegin();
    onChange && onChange(rects.map((r) => set.has(r.id) ? { ...r, [axis]: r[axis] + d } : { ...r }));
  };
  const alignIcon = (key) => {
    const common = { width: 15, height: 15, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true' };
    if (key === 'left') return <svg {...common}><path d="M4 3v10" /><path d="M5.5 5H13" /><path d="M5.5 10H11" /></svg>;
    if (key === 'centerH') return <svg {...common}><path d="M8 3v10" /><path d="M3 5h10" /><path d="M5 10h6" /></svg>;
    if (key === 'right') return <svg {...common}><path d="M12 3v10" /><path d="M3 5h7.5" /><path d="M5 10h5.5" /></svg>;
    if (key === 'top') return <svg {...common}><path d="M3 4h10" /><path d="M5 5.5v7.5" /><path d="M10 5.5v5.5" /></svg>;
    if (key === 'middleV') return <svg {...common}><path d="M3 8h10" /><path d="M5 3v10" /><path d="M10 5v6" /></svg>;
    return <svg {...common}><path d="M3 12h10" /><path d="M5 3v7.5" /><path d="M10 5v5.5" /></svg>;
  };

  const ordered = rects.map((r, i) => ({ r, i })).sort((a, b) => a.r.z - b.r.z);
  const stopDeleteEvent = (e) => {
    e.preventDefault(); e.stopPropagation();
    if (e.nativeEvent && e.nativeEvent.stopImmediatePropagation) e.nativeEvent.stopImmediatePropagation();
  };
  const deleteFromButton = (e, r) => {
    stopDeleteEvent(e);
    const key = (r.id || '') + '|' + (r.src || r.url || '');
    const now = Date.now();
    if (deleteGuardRef.current.key === key && now - deleteGuardRef.current.at < 300) return;
    deleteGuardRef.current = { key, at: now };
    onDeleteItem && onDeleteItem(r);
  };
  const resizeFromMenu = (e) => {
    stopDeleteEvent(e);
    const r = menu && rects[menu.idx];
    if (!r) return;
    setMultiIds([r.id]);
    onSelect && onSelect(r.id);
    setResizeModeId(r.id);
    setMenu(null);
  };
  const deleteFromMenu = (e) => {
    const r = menu && rects[menu.idx];
    if (!r) return;
    deleteFromButton(e, r);
    setMenu(null);
  };
  const CORNERS = ['nw', 'ne', 'sw', 'se'];
  const clampDecoPos = (p, d) => {
    const x = Number(p && p.x), y = Number(p && p.y);
    return {
      x: Math.max(0, Math.min(550 - (d.w || 90), Number.isFinite(x) ? x : 0)),
      y: Math.max(0, Math.min(550 - (d.h || 90), Number.isFinite(y) ? y : 0)),
    };
  };
  const onDecoDown = (e, pos) => {
    if (!editable || !deco || !onDecoPosChange || e.button === 2) return;
    e.stopPropagation(); e.preventDefault();
    onSelectDeco && onSelectDeco();
    decoDrag.current = { startX: e.clientX, startY: e.clientY, orig: pos };
    window.addEventListener('pointermove', onDecoMove);
    window.addEventListener('pointerup', onDecoUp);
  };
  const onDecoMove = (e) => {
    const dr = decoDrag.current; if (!dr || !deco) return;
    onDecoPosChange(clampDecoPos({ x: dr.orig.x + (e.clientX - dr.startX) / scale, y: dr.orig.y + (e.clientY - dr.startY) / scale }, deco));
  };
  const onDecoUp = () => {
    decoDrag.current = null;
    window.removeEventListener('pointermove', onDecoMove);
    window.removeEventListener('pointerup', onDecoUp);
  };
  const onDecoResizeDown = (e) => {
    if (!editable || !deco || !onDecoScale || e.button === 2) return;
    e.stopPropagation(); e.preventDefault();
    onSelectDeco && onSelectDeco();
    decoResize.current = { sx: e.clientX, sy: e.clientY, scale: Number(deco.scale) || 1 };
    const mv = (ev) => {
      const dr = decoResize.current; if (!dr) return;
      onDecoScale(Math.max(0.5, Math.min(2.4, +(dr.scale + ((ev.clientX - dr.sx) + (ev.clientY - dr.sy)) / 180).toFixed(3))));
    };
    const up = () => { decoResize.current = null; window.removeEventListener('pointermove', mv); window.removeEventListener('pointerup', up); };
    window.addEventListener('pointermove', mv); window.addEventListener('pointerup', up);
  };

  const decoEls = (() => {
    const out = []; let logoBottom = 40;
    if (logos && logos.length) {
      const row = logoRow550(logos);
      const arr = row.items;
      const top = 40;
      for (let i = arr.length - 1; i >= 0; i--) {
        const w = arr[i].w, h = arr[i].h;
        // Top-right anchor: right edge fixed at precomputed rightEdge, grows left/down
        const x = arr[i].rightEdge - w;
        const sel = selLogoIdx === i;
        out.push(<img key={'lg' + i} data-logo-idx={i} src={arr[i].url} alt="" draggable={false}
          onPointerDown={(e) => { if (!onSelectLogo) return; e.stopPropagation(); e.preventDefault(); onSelectLogo(i); }}
          style={{ position: 'absolute', left: x, top: top, width: w, height: h, objectFit: 'contain', pointerEvents: onSelectLogo ? 'auto' : 'none', cursor: onSelectLogo ? 'pointer' : 'default', zIndex: 4000, outline: sel ? '1.5px solid #4E4CDB' : 'none', outlineOffset: 2 }} />);
      }
      logoBottom = top + row.rowH;
    }
    if (deco && window.DecoBadge) {
      const def = { x: 550 - 50 - (deco.w || 90), y: logos && logos.length ? logoBottom + 30 : 40 };
      const pos = clampDecoPos(decoPos && typeof decoPos === 'object' ? decoPos : def, deco);
      const ds = Number(deco.scale) || 1;
      const bw = deco.baseW || (deco.w || 90) / ds;
      const bh = deco.baseH || (deco.h || 90) / ds;
      out.push(<div key="deco" data-deco-overlay="550" onPointerDown={(e) => onDecoDown(e, pos)} style={{ position: 'absolute', left: pos.x, top: pos.y, width: deco.w || bw * ds, height: deco.h || bh * ds, zIndex: 4000, pointerEvents: editable && onDecoPosChange ? 'auto' : 'none', cursor: editable && onDecoPosChange ? 'move' : 'default', touchAction: 'none', userSelect: 'none', outline: decoSelected ? '1.5px solid #4E4CDB' : 'none', outlineOffset: 3 }}>
        <div style={{ width: bw, height: bh, transform: `scale(${ds})`, transformOrigin: 'top left' }}>{React.createElement(window.DecoBadge, { deco })}</div>
        {decoSelected && editable && onDecoScale && <span data-deco-resize="550" title="부가정보 크기 조절" onPointerDown={onDecoResizeDown} style={{ position: 'absolute', right: -8, bottom: -8, width: 15, height: 15, border: '2px solid #fff', borderRadius: 2, background: '#4E4CDB', cursor: 'nwse-resize', boxShadow: '0 1px 5px rgba(0,0,0,.18)' }} />}
      </div>);
    }
    return out.length ? out : null;
  })();

  const menuPortal = menu && editable && document.body ? (() => {
    const w = 168;
    const h = onDeleteItem ? 150 : 114;
    const left = Math.max(8, Math.min(window.innerWidth - w - 8, menu.x));
    const top = Math.max(8, Math.min(window.innerHeight - h - 8, menu.y));
    return ReactDOM.createPortal(
      <div className="ap-ctx" style={{ position: 'fixed', left, top, zIndex: 100000 }} onPointerDown={(e) => e.stopPropagation()}>
        <button className="ap-ctx-item" onPointerDown={() => changeZ(menu.idx, 'front')}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M5 12l7-7 7 7" /></svg>앞으로
        </button>
        <button className="ap-ctx-item" onPointerDown={() => changeZ(menu.idx, 'back')}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12l7 7 7-7" /></svg>뒤로
        </button>
        <button className="ap-ctx-item" onPointerDown={resizeFromMenu}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M4 9V4h5" /><path d="M20 15v5h-5" /><path d="M4 4l6 6" /><path d="M20 20l-6-6" /></svg>사이즈 변경
        </button>
        {onDeleteItem && (
          <button className="ap-ctx-item" onPointerDown={deleteFromMenu}>
            <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18" /><path d="M8 6V4h8v2" /><path d="M19 6l-1 14H6L5 6" /></svg>삭제
          </button>
        )}
      </div>,
      document.body
    );
  })() : null;

  return (
    <div className="ap-stage-wrap" ref={wrapRef} style={{ width: dispW, height: dispW, margin: '0px', borderStyle: 'none', borderColor: 'rgb(204, 204, 204)', borderRadius: '0px' }}>
      <div className="ap-stage" style={{ width: 550, height: 550, transform: `scale(${scale})` }}
        onContextMenu={openMenuAtPoint}
        onPointerDown={(e) => { if (editable && e.button !== 2) { onSelect && onSelect(null); onSelectLogo && onSelectLogo(null); setMultiIds([]); setMenu(null); } }}>
        <div className="ap-canvas-bg" style={{ borderStyle: 'none' }} />
        {ordered.map(({ r, i }) => {
          const sel = selectedId === r.id || multiIds.includes(r.id);
          const showHandles = (sel || resizeModeId === r.id) && editable && multiIds.length < 2;
          return (
            <div key={r.id} className={'ap-item' + (sel ? ' sel' : '') + (editable ? ' edit' : '')}
              style={{ left: r.x, top: r.y, width: r.w, height: r.h, zIndex: r.z + 1000 }}
              onPointerDown={(e) => onItemDown(e, i, 'move')}
              onContextMenu={(e) => openMenu(e, i)}>
              <img src={r.url} alt="" draggable={false} style={{ ...(r.id === '__modelCorner' ? { objectPosition: 'right top' } : {}), ...((window.BannerImageTools && window.BannerImageTools.imageStyle(window.BannerImageTools.forSource ? window.BannerImageTools.forSource(imageAdjust, imageAdjustBySource, r.src || r.url) : imageAdjust)) || {}) }} />
              {showHandles && CORNERS.map((c) =>
                <span key={c} className={'ap-handle ' + c} style={{ transform: `scale(${1 / scale})` }} onPointerDown={(e) => onItemDown(e, i, c)} />
              )}
              {editable && onDeleteItem && (
                <button
                  type="button"
                  title="이미지 삭제"
                  aria-label="이미지 삭제"
                  onPointerDown={stopDeleteEvent}
                  onMouseDown={stopDeleteEvent}
                  onPointerUp={(e) => deleteFromButton(e, r)}
                  onClick={(e) => deleteFromButton(e, r)}
                  style={{ position: 'absolute', top: 0, right: 0, minWidth: 22, minHeight: 22, width: Math.max(22, Math.round(20 / scale)), height: Math.max(22, Math.round(20 / scale)), background: 'rgba(28,28,34,.82)', border: 'none', borderRadius: '0 0 0 4px', color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 0, zIndex: 9500, lineHeight: 1, pointerEvents: 'auto', touchAction: 'none' }}
                >
                  <svg width={Math.round(9 / scale)} height={Math.round(9 / scale)} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18M6 6l12 12" /></svg>
                </button>
              )}
            </div>
          );
        })}

        {/* 그룹 바운딩 박스 (2개 이상 선택 시) */}
        {groupBbox && editable && (
          <div style={{ position: 'absolute', left: groupBbox.x - 3, top: groupBbox.y - 3, width: groupBbox.w + 6, height: groupBbox.h + 6, border: '1.5px dashed #4E4CDB', borderRadius: 3, pointerEvents: 'none', zIndex: 8900, boxSizing: 'border-box' }}>
            {/* SE(우하단) 리사이즈 핸들 */}
            <span
              style={{ position: 'absolute', right: -5, bottom: -5, width: 10, height: 10, background: '#fff', border: '1.5px solid #4E4CDB', borderRadius: 2, cursor: 'nwse-resize', pointerEvents: 'auto', zIndex: 8901, transform: `scale(${1 / scale})`, transformOrigin: 'bottom right' }}
              onPointerDown={onGroupHandleDown}
            />
          </div>
        )}

        {editable && selectedTargetIds.length > 0 && (
          <div
            className="ap-align-toolbar"
            onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); }}
            style={{ position: 'absolute', left: '50%', bottom: 10, transform: 'translateX(-50%)', display: 'flex', alignItems: 'center', gap: 3, padding: '4px', borderRadius: 8, background: 'rgba(255,255,255,.94)', border: '1px solid rgba(0,0,0,.12)', boxShadow: '0 6px 18px rgba(0,0,0,.14)', zIndex: 9805, pointerEvents: 'auto' }}
          >
            {[
              ['left', '왼쪽 정렬', 'x', 'start'],
              ['centerH', '가로 가운데 정렬', 'x', 'center'],
              ['right', '오른쪽 정렬', 'x', 'end'],
              ['top', '상단 정렬', 'y', 'start'],
              ['middleV', '세로 가운데 정렬', 'y', 'center'],
              ['bottom', '하단 정렬', 'y', 'end'],
            ].map(([key, title, axis, pos]) => (
              <button
                key={key}
                type="button"
                title={title}
                aria-label={title}
                onPointerDown={alignSelection(axis, pos)}
                style={{ width: 28, height: 26, borderRadius: 6, border: '1px solid #e0e0ea', background: '#fff', color: '#4E4CDB', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', padding: 0 }}
              >
                {alignIcon(key)}
              </button>
            ))}
          </div>
        )}

        {/* 여백 가이드라인 오버레이 (편집 캔버스 전용) */}
        {!faintSafe && guideOn && (
          <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 9000 }}>
            <SafeGuideOverlay sx={SAFE_550.x} sy={SAFE_550.y} cw={SAFE_550.cw} ch={SAFE_550.ch} />
          </div>
        )}

        {/* 기존 세이프 영역 표시 (faintSafe 미리보기 + 가이드 OFF 시 폴백) */}
        {(faintSafe || !guideOn) && (
          <div className={'ap-safe' + (faintSafe ? ' faint' : '')} style={{ left: 50, top: 40, right: 50, bottom: 40 }}>
            {!faintSafe && <span className="ap-safe-tag">SAFE 450 × 470</span>}
          </div>
        )}
        {decoEls}
      </div>
      {menuPortal}
    </div>
  );
}

function APMetricRow({ k, v, tone, strong }) {
  return (
    <div className={'ap-mrow' + (strong ? ' strong' : '')}>
      <span className="ap-mk">{k}</span>
      <span className={'ap-mv' + (tone ? ' t-' + tone : '')}>{v}</span>
    </div>
  );
}

const toneColor = { good: '#0E9F6E', ok: '#3A7BD5', warn: '#D98324', bad: '#D64545' };

function APResultCard({ cand, rects, metrics, rank, primary, selectedId, onSelect, onChange, onReset, onApplyGuide, onMakePrimary, onDownload, count, scorer, onUndo, canUndo, onRedo, canRedo, onBegin, logos, deco, decoPos, onDecoPosChange, onDecoScale, decoSelected, onSelectDeco, onDeleteItem, guideOn = true, selLogoIdx, onSelectLogo, onLogoScale, imageAdjust, imageAdjustBySource, onSelectionChange }) {
  const grade = window.AP550.recognitionGrade(metrics.exposure);
  const occWarn = metrics.occlusion > 22;
  const NP = window.NukkiPattern;
  const patName = NP && NP.labelOf(cand);
  return (
    <div className={'ap-card' + (primary ? ' active' : '')}>
      <div className="ap-card-head">
        <span className={'ap-rank r' + rank}>{rank}순위</span>
        <span className="ap-card-titles">
          <span className="ap-card-name">{patName || cand.strategyLabel}</span>
          <span className="ap-card-desc">{cand.rowsLabel} · 점수 {cand.scoring.score.toFixed(2)}</span>
        </span>
        {rank === 1 && <span className="ap-rec-badge"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>스코어러 추천</span>}
      </div>

      <APStage rects={rects} dispW={364} editable={primary} selectedId={primary ? selectedId : null} onSelect={onSelect} onChange={onChange} bounds={cand.bounds} scorer={scorer} onBegin={onBegin} noReflow logos={logos} deco={deco} decoPos={decoPos} onDecoPosChange={primary ? onDecoPosChange : null} onDecoScale={primary ? onDecoScale : null} decoSelected={primary ? decoSelected : false} onSelectDeco={primary ? onSelectDeco : null} onDeleteItem={primary ? onDeleteItem : null} guideOn={guideOn} selLogoIdx={primary ? selLogoIdx : null} onSelectLogo={primary ? onSelectLogo : null} imageAdjust={imageAdjust} imageAdjustBySource={imageAdjustBySource} onSelectionChange={primary ? onSelectionChange : null} />
      {primary && <LogoSizeSlider logos={logos} selIdx={selLogoIdx} onScale={onLogoScale} />}

      {/* 다중 선택 힌트 (편집 카드에서만) */}
      {primary && (
        <p style={{ margin: '4px 0 0', fontSize: 11, color: '#999', textAlign: 'center', lineHeight: 1.4 }}>
          Shift+클릭으로 여러 제품 선택 후 핸들로 함께 확대
        </p>
      )}

      <window.APMetFold recline={<span style={{ display: 'inline-flex', alignItems: 'center', gap: '7px', color: toneColor[grade.tone], fontSize: '13px', fontWeight: 700, letterSpacing: '-.01em' }}><span className="ap-dot" style={{ background: toneColor[grade.tone] }} />{grade.label} — 유효 노출 기준</span>}>
        <APMetricRow k="실루엣 적합도" v={metrics.silhouette != null ? Math.round(metrics.silhouette * 100) + '%' : '—'} tone={metrics.silhouette != null ? (metrics.silhouette >= 0.85 ? 'good' : (metrics.silhouette < 0.66 ? 'warn' : null)) : null} strong />
        <APMetricRow k="유효 노출" v={metrics.exposure + '%'} tone={grade.tone} />
        <APMetricRow k="제품 가려짐" v={metrics.occlusion + '%'} tone={occWarn ? 'warn' : null} />
        <APMetricRow k="여백률" v={metrics.whitespace + '%'} tone={metrics.whitespace > 38 ? 'warn' : (metrics.whitespace <= 28 ? 'good' : null)} />
        <APMetricRow k="세이프 밀도" v={metrics.density + '%'} />
        <APMetricRow k="배치 유형" v={patName || cand.strategyLabel} />
        <APMetricRow k="행 구성" v={cand.rowsLabel} />
        <APMetricRow k="겹친 쌍 / 최대" v={metrics.pairs + '쌍 · ' + metrics.maxPair + '%'} />
      </window.APMetFold>

      <div className="ap-card-actions">
        {primary ?
          <>
            <button className="ap-btn icon" title="되돌리기 (⌘Z)" onClick={onUndo} disabled={!canUndo}><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M9 14L4 9l5-5" /><path d="M4 9h11a5 5 0 0 1 0 10h-1" /></svg></button>
            <button className="ap-btn icon" title="다시 실행 (⇧⌘Z)" onClick={onRedo} disabled={!canRedo}><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"><path d="M15 14l5-5-5-5" /><path d="M20 9H9a5 5 0 0 0 0 10h1" /></svg></button>
            <button className="ap-btn ghost" onClick={onReset}>배치 초기화</button>
            {onApplyGuide && <button className="ap-btn ghost" onClick={onApplyGuide}>규칙 적용</button>}
          </> :
          <button className="ap-btn ghost block" onClick={onMakePrimary}>이 후보로 조정하기</button>
        }
      </div>
    </div>
  );
}

function APMiniCanvas({ cand, rank, active, onClick, onHide }) {
  const top2 = rank <= 2;
  const NP = window.NukkiPattern;
  const patName = NP && NP.labelOf(cand);
  return (
    <div className={'ap-mini' + (active ? ' active' : '') + (top2 ? ' top2' : '')}>
      <button className="ap-mini-btn" onClick={onClick} title={(patName || cand.strategyLabel) + ' · ' + cand.rowsLabel}>
        <div className="ap-mini-canvas">
          <APStage rects={cand.rects} dispW={100} editable={false} faintSafe />
        </div>
        <div className="ap-mini-meta">
          <span className="ap-mini-rank">{rank}</span>
          <span className="ap-mini-score">{cand.scoring.score.toFixed(2)}</span>
        </div>
        {patName && <div style={{ fontSize: '10px', fontWeight: 700, color: '#6b6b78', padding: '0 2px 4px', lineHeight: 1.2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{patName}</div>}
        <div className="ap-mini-bar"><i style={{ width: Math.max(4, Math.min(100, cand.scoring.score * 70 + 30)) + '%' }} /></div>
      </button>
    </div>
  );
}

/* LogoSizeSlider — per-logo 너비·높이 개별 슬라이더.
   onScale(idx, axis, value): axis='w'|'h', value=scaleW|scaleH multiplier. */
function LogoSizeSlider({ logos, selIdx, onScale }) {
  if (selIdx == null || !logos || !logos[selIdx] || !onScale) return null;
  const g = logos[selIdx];
  const sw = logoScaleW550(g), sh = logoScaleH550(g);
  const row = { display: 'flex', alignItems: 'center', gap: 8 };
  const lbl = { fontSize: 12, fontWeight: 700, color: '#43434e', whiteSpace: 'nowrap', minWidth: 22 };
  const val = { fontSize: 12, minWidth: 38, textAlign: 'right', color: '#1c1c22' };
  return (
    <div className="ap-logo-size" style={{ margin: '8px 0 0', padding: '0 2px', display: 'flex', flexDirection: 'column', gap: 5 }}>
      <div style={row}>
        <span style={lbl}>너비</span>
        <input type="range" min="0.25" max="3" step="0.05" value={sw}
               onChange={(e) => onScale(selIdx, 'w', parseFloat(e.target.value))}
               style={{ flex: 1, accentColor: '#4E4CDB' }} />
        <b style={val}>{Math.round(sw * 100)}%</b>
      </div>
      <div style={row}>
        <span style={lbl}>높이</span>
        <input type="range" min="0.25" max="3" step="0.05" value={sh}
               onChange={(e) => onScale(selIdx, 'h', parseFloat(e.target.value))}
               style={{ flex: 1, accentColor: '#4E4CDB' }} />
        <b style={val}>{Math.round(sh * 100)}%</b>
      </div>
    </div>
  );
}

Object.assign(window, { APStage, APResultCard, APMiniCanvas, APMetricRow, LogoSizeSlider });
