"use client"
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Moveable from 'react-moveable'
import type { Frame, WObject } from '@/lib/wysiwyg/schema'

function snap8(v: number) {
  return Math.round(v / 8) * 8
}

// (removed unused type guard)

// 테스트 환경(jsdom)에서 react-moveable이 사용하는 elementFromPoint가 없을 수 있어 안전한 폴리필을 추가
type DocWithEFP = Document & { elementFromPoint?: (x: number, y: number) => Element | null }
if (typeof document !== 'undefined') {
  const d = document as DocWithEFP
  if (typeof d.elementFromPoint !== 'function') {
    d.elementFromPoint = (_x: number, _y: number) => { void _x; void _y; return null }
  }
}

export type CanvasProps = {
  frame: Frame
  onChange?: (next: Frame) => void
  onSelectIds?: (ids: string[]) => void
  enableGuides?: boolean
}

export function Canvas({ frame, onChange, onSelectIds, enableGuides = true }: CanvasProps) {
  const [model, setModel] = useState<Frame>(frame)
  // 외부 프레임 변경(예: 빌더에서 PropsPanel 수정/객체 추가)이 들어오면 동기화
  useEffect(() => { setModel(frame) }, [frame])
  const initialId = model.layers[0]?.objects[0]?.id ?? null
  const [selectedIds, setSelectedIds] = useState<string[]>(initialId ? [initialId] : [])
  const selectedId = selectedIds[selectedIds.length - 1] ?? null
  const draggingRef = useRef<{ id: string; startX: number; startY: number; origX: number; origY: number } | null>(null)
  const [isDragging, setIsDragging] = useState(false)
  const resizingRef = useRef<{ id: string; dir: 'w' | 'n' | 'e' | 's' | 'ne' | 'se' | 'nw' | 'sw'; startX: number; startY: number; origX: number; origY: number; origW: number; origH: number } | null>(null)
  const resizeGroupRef = useRef<{ ids: string[]; orig: Record<string, { x: number; y: number; w: number; h: number }> } | null>(null)
  const rotatingRef = useRef<{ id: string; startX: number; startY: number; origDeg: number } | null>(null)
  const rotateGroupRef = useRef<{ ids: string[]; orig: Record<string, { d: number }> } | null>(null)
  const [showGuides, setShowGuides] = useState(false)
  const [guidePos, setGuidePos] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
  const [marquee, setMarquee] = useState<{
    active: boolean
    x0: number
    y0: number
    x1: number
    y1: number
  }>({ active: false, x0: 0, y0: 0, x1: 0, y1: 0 })
  // 선택된 객체 DOM을 식별하기 위한 ref(선택자 기반 target으로 전환)
  const selectedElRef = useRef<HTMLDivElement | null>(null)

  const selected = useMemo(() => {
    for (const layer of model.layers) {
      const obj = layer.objects.find((o) => o.id === selectedId)
      if (obj) return obj
    }
    return null
  }, [model, selectedId])

  // enableGuides가 꺼지면 즉시 가이드를 숨긴다
  useEffect(() => {
    if (!enableGuides && showGuides) setShowGuides(false)
    if (!enableGuides && isDragging) setIsDragging(false)
  }, [enableGuides, showGuides, isDragging])

  // a11y 라이브 리전 텍스트: 선택 개수 및 가이드 좌표를 폴라이트로 알림
  const ariaLiveText = useMemo(() => {
    const count = selectedIds.length
    const base = `선택 ${count}개`
    const pos = selected ? `, 위치 x=${selected.x}, y=${selected.y}` : ''
    const guide = (enableGuides && (showGuides || isDragging)) ? `, 가이드 x=${guidePos.x}, y=${guidePos.y}` : ''
    return `${base}${pos}${guide}`
  }, [selectedIds.length, showGuides, isDragging, enableGuides, guidePos.x, guidePos.y, selected])

  const updateObject = useCallback((id: string, updater: (o: WObject) => WObject) => {
    setModel((prev) => {
      const next: Frame = { ...prev, layers: prev.layers.map((ly) => ({ ...ly, objects: ly.objects.map((o) => (o.id === id ? updater(o) : o)) })) }
      onChange?.(next)
      return next
    })
  }, [onChange])

  const updateObjects = useCallback((ids: string[], updater: (o: WObject) => WObject) => {
    setModel((prev) => {
      const idSet = new Set(ids)
      const next: Frame = {
        ...prev,
        layers: prev.layers.map((ly) => ({
          ...ly,
          objects: ly.objects.map((o) => (idSet.has(o.id) ? updater(o) : o)),
        })),
      }
      onChange?.(next)
      return next
    })
  }, [onChange])

  const onKeyDown = useCallback((e: React.KeyboardEvent) => {
    if (!selected) return
    let dx = 0, dy = 0
    let step = 1
    let useSnap = true
    // Shift: 미세 이동(1px, 스냅 없음), Alt: 가속 이동(16px, 스냅 적용)
    if (e.shiftKey) { step = 1; useSnap = false }
    else if (e.altKey) { step = 16; useSnap = true }

    if (e.key === 'ArrowLeft') dx = -step
    else if (e.key === 'ArrowRight') dx = step
    else if (e.key === 'ArrowUp') dy = -step
    else if (e.key === 'ArrowDown') dy = step
    else return
    e.preventDefault()
    updateObject(selected.id, (o) => {
      const tx = o.x + dx
      const ty = o.y + dy
      const nx = useSnap ? snap8(tx) : tx
      const ny = useSnap ? snap8(ty) : ty
      return { ...o, x: nx, y: ny }
    })
  }, [selected, updateObject])

  const onMouseDownObj = useCallback((e: React.MouseEvent, o: WObject) => {
    if (e.shiftKey) {
      setSelectedIds((prev) => {
        const exists = prev.includes(o.id)
        if (exists) return prev.filter((id) => id !== o.id)
        const next = [...prev, o.id]
        try { onSelectIds?.(next) } catch {}
        return next
      })
    } else {
      setSelectedIds((prev) => {
        // if already selected, preserve current multi-selection
        if (prev.includes(o.id)) {
          try { onSelectIds?.(prev) } catch {}
          return prev
        }
        const next = [o.id]
        try { onSelectIds?.(next) } catch {}
        return next
      })
    }
    draggingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origX: o.x, origY: o.y }
    setIsDragging(true)
    if (enableGuides) {
      setShowGuides(true)
      setGuidePos({ x: snap8(o.x), y: snap8(o.y) })
    }
  }, [selectedIds, model.layers, onSelectIds, enableGuides])

  const onMouseMoveCanvas = useCallback((e: React.MouseEvent) => {
    // guides 비활성화 상태에서 남아있다면 즉시 숨김
    if (!enableGuides && showGuides) setShowGuides(false)
    // 마퀴 선택 처리 우선
    if (marquee.active) {
      const x1 = e.clientX
      const y1 = e.clientY
      setMarquee((m) => ({ ...m, x1, y1 }))
      // 교차하는 객체 선택 갱신
      const minX = Math.min(marquee.x0, x1)
      const minY = Math.min(marquee.y0, y1)
      const maxX = Math.max(marquee.x0, x1)
      const maxY = Math.max(marquee.y0, y1)
      const hits: string[] = []
      for (const ly of model.layers) {
        for (const o of ly.objects) {
          const ol = o.x
          const ot = o.y
          const or = o.x + o.width
          const ob = o.y + o.height
          const inter = !(or < minX || ob < minY || ol > maxX || ot > maxY)
          if (inter) hits.push(o.id)
        }
      }
      setSelectedIds(hits)
      try { onSelectIds?.(hits) } catch {}
      return
    }
    const rot = rotatingRef.current
    if (rot) {
      const dx = e.clientX - rot.startX
      let next = rot.origDeg + dx
      if (!e.shiftKey) {
        next = Math.round(next / 15) * 15
      }
      if (selectedIds.length > 1 && rotateGroupRef.current) {
        const { ids, orig } = rotateGroupRef.current
        const base = orig[rot.id]
        const dd = next - base.d
        updateObjects(ids, (o) => {
          const o0 = orig[o.id]
          if (!o0) return o
          return { ...o, rotate: o0.d + dd }
        })
      } else {
        updateObject(rot.id, (o) => ({ ...o, rotate: next }))
      }
      return
    }
    const r = resizingRef.current
    if (r) {
      const dx = e.clientX - r.startX
      const dy = e.clientY - r.startY
      let nx = r.origX
      let ny = r.origY
      let nw = r.origW
      let nh = r.origH

      if (r.dir === 'e') {
        nw = Math.max(8, snap8(r.origW + dx))
      } else if (r.dir === 's') {
        nh = Math.max(8, snap8(r.origH + dy))
      } else if (r.dir === 'w') {
        // left edge moves, right edge fixed
        const tentativeLeft = snap8(r.origX + dx)
        const right = r.origX + r.origW
        nx = Math.min(tentativeLeft, right - 8)
        nw = Math.max(8, right - nx)
      } else if (r.dir === 'n') {
        // top edge moves, bottom fixed
        const tentativeTop = snap8(r.origY + dy)
        const bottom = r.origY + r.origH
        ny = Math.min(tentativeTop, bottom - 8)
        nh = Math.max(8, bottom - ny)
      } else if (r.dir === 'ne') {
        // top moves like 'n', right like 'e'
        const tentativeTop = snap8(r.origY + dy)
        const bottom = r.origY + r.origH
        ny = Math.min(tentativeTop, bottom - 8)
        nh = Math.max(8, bottom - ny)
        nw = Math.max(8, snap8(r.origW + dx))
      } else if (r.dir === 'se') {
        // bottom-right: origin stays, width/height increase
        nw = Math.max(8, snap8(r.origW + dx))
        nh = Math.max(8, snap8(r.origH + dy))
      } else if (r.dir === 'nw') {
        // top-left: move origin up/left, keep bottom-right fixed
        const right = r.origX + r.origW
        const bottom = r.origY + r.origH
        const tentativeLeft = snap8(r.origX + dx)
        const tentativeTop = snap8(r.origY + dy)
        nx = Math.min(tentativeLeft, right - 8)
        ny = Math.min(tentativeTop, bottom - 8)
        nw = Math.max(8, right - nx)
        nh = Math.max(8, bottom - ny)
      } else if (r.dir === 'sw') {
        // bottom-left: move left edge, increase height
        const right = r.origX + r.origW
        const tentativeLeft = snap8(r.origX + dx)
        nx = Math.min(tentativeLeft, right - 8)
        nw = Math.max(8, right - nx)
        nh = Math.max(8, snap8(r.origH + dy))
      }

      // single selection vs multi-selection 적용
      if (selectedIds.length > 1 && resizeGroupRef.current) {
        const { ids, orig } = resizeGroupRef.current
        const base = orig[r.id]
        const dw = nw - base.w
        const dh = nh - base.h
        // 서/북 방향 포함 시 원점 이동량을 그룹 전체에 동일 적용
        const dxShift = nx - r.origX
        const dyShift = ny - r.origY
        updateObjects(ids, (o) => {
          const o0 = orig[o.id]
          if (!o0) return o
          const next: typeof o = {
            ...o,
            width: Math.max(8, o0.w + dw),
            height: Math.max(8, o0.h + dh),
          }
          // 좌측/상단 이동이 있는 경우에만 좌표 이동 적용
          if (dxShift !== 0) next.x = o0.x + dxShift
          if (dyShift !== 0) next.y = o0.y + dyShift
          return next
        })
      } else {
        updateObject(r.id, (o) => ({ ...o, x: nx, y: ny, width: nw, height: nh }))
      }
      return
    }
    const d = draggingRef.current
    if (d) {
      if (enableGuides && !showGuides) setShowGuides(true)
      const dx = e.clientX - d.startX
      const dy = e.clientY - d.startY
      // compute raw target
      const rawLeft = d.origX + dx
      const rawTop = d.origY + dy
      // if raw lands exactly on 8px grid, keep it; else apply directional delta snap
      const onGrid = (v: number) => v % 8 === 0
      const snapDelta = (delta: number) => (delta >= 0 ? Math.ceil(delta / 8) : Math.floor(delta / 8)) * 8
      let nx = onGrid(rawLeft) ? rawLeft : d.origX + snapDelta(dx)
      let ny = onGrid(rawTop) ? rawTop : d.origY + snapDelta(dy)

      // object-to-object snapping (edge > center > grid) with threshold 8px
      const threshold = 8
      // find current selected object dimensions
      let selW = 0, selH = 0
      for (const ly of model.layers) {
        const ob = ly.objects.find((o) => o.id === d.id)
        if (ob) { selW = ob.width; selH = ob.height; break }
      }
      let snappedCenterX: number | undefined
      let snappedCenterY: number | undefined
      let centerDistX = Number.POSITIVE_INFINITY
      let centerDistY = Number.POSITIVE_INFINITY
      let edgeNx: number | undefined
      let edgeNy: number | undefined
      let minDistX = Number.POSITIVE_INFINITY
      let minDistY = Number.POSITIVE_INFINITY
      for (const layer of model.layers) {
        for (const obj of layer.objects) {
          if (obj.id === d.id) continue
          // center snap
          const otherCenterX = obj.x + obj.width / 2
          const selCenterX = rawLeft + selW / 2
          const dxCenter = Math.abs(selCenterX - otherCenterX)
          if (dxCenter <= threshold && dxCenter < centerDistX) {
            centerDistX = dxCenter
            snappedCenterX = otherCenterX
          }
          const otherCenterY = obj.y + obj.height / 2
          const selCenterY = rawTop + selH / 2
          const dyCenter = Math.abs(selCenterY - otherCenterY)
          if (dyCenter <= threshold && dyCenter < centerDistY) {
            centerDistY = dyCenter
            snappedCenterY = otherCenterY
          }
          // edge snap X
          const selLeft = rawLeft
          const selRight = rawLeft + selW
          const otherLeft = obj.x
          const otherRight = obj.x + obj.width
          const xPairs: Array<{ dist: number; nx: number }> = [
            { dist: Math.abs(selLeft - otherLeft), nx: otherLeft },
            { dist: Math.abs(selLeft - otherRight), nx: otherRight },
            { dist: Math.abs(selRight - otherLeft), nx: otherLeft - selW },
            { dist: Math.abs(selRight - otherRight), nx: otherRight - selW },
          ]
          for (const p of xPairs) {
            if (p.dist <= threshold && p.dist < minDistX) {
              minDistX = p.dist
              edgeNx = p.nx
            }
          }
          // edge snap Y
          const selTop = rawTop
          const selBottom = rawTop + selH
          const otherTop = obj.y
          const otherBottom = obj.y + obj.height
          const yPairs: Array<{ dist: number; ny: number }> = [
            { dist: Math.abs(selTop - otherTop), ny: otherTop },
            { dist: Math.abs(selTop - otherBottom), ny: otherBottom },
            { dist: Math.abs(selBottom - otherTop), ny: otherTop - selH },
            { dist: Math.abs(selBottom - otherBottom), ny: otherBottom - selH },
          ]
          for (const p of yPairs) {
            if (p.dist <= threshold && p.dist < minDistY) {
              minDistY = p.dist
              edgeNy = p.ny
            }
          }
        }
      }
      if (typeof edgeNx === 'number') nx = edgeNx
      else if (typeof snappedCenterX === 'number') nx = snappedCenterX - selW / 2
      if (typeof edgeNy === 'number') ny = edgeNy
      else if (typeof snappedCenterY === 'number') ny = snappedCenterY - selH / 2
      setGuidePos({ x: nx, y: ny })
      if (selectedIds.length > 1) {
        const ddx = nx - d.origX
        const ddy = ny - d.origY
        updateObjects(selectedIds, (o) => ({ ...o, x: o.x + ddx, y: o.y + ddy }))
      } else {
        updateObject(d.id, (o) => ({ ...o, x: nx, y: ny }))
      }
    }
  }, [updateObject, updateObjects, marquee.active, marquee.x0, marquee.y0, model.layers, selectedIds, selectedIds.length, enableGuides, onSelectIds, showGuides])

  const onMouseUpCanvas = useCallback(() => {
    if (marquee.active) {
      setMarquee((m) => ({ ...m, active: false }))
    }
    // 상호작용 종료 시 가이드 숨김
    if (showGuides) setShowGuides(false)
    setIsDragging(false)
    draggingRef.current = null
    resizingRef.current = null
    resizeGroupRef.current = null
    rotatingRef.current = null
  }, [marquee.active, showGuides])

  const onMouseDownResize = useCallback((e: React.MouseEvent, o: WObject, dir: 'w' | 'n' | 'e' | 's' | 'ne' | 'se' | 'nw' | 'sw') => {
    e.stopPropagation()
    resizingRef.current = { id: o.id, dir, startX: e.clientX, startY: e.clientY, origX: o.x, origY: o.y, origW: o.width, origH: o.height }
    // capture originals for group resize
    const ids = Array.from(new Set([...selectedIds, o.id]))
    const orig: Record<string, { x: number; y: number; w: number; h: number }> = {}
    for (const ly of model.layers) {
      for (const ob of ly.objects) {
        if (ids.includes(ob.id)) orig[ob.id] = { x: ob.x, y: ob.y, w: ob.width, h: ob.height }
      }
    }
    resizeGroupRef.current = { ids, orig }
  }, [selectedIds, model.layers])

  const onMouseDownRotate = useCallback((e: React.MouseEvent, o: WObject) => {
    e.stopPropagation()
    rotatingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origDeg: o.rotate }
    const ids = Array.from(new Set([...selectedIds, o.id]))
    const orig: Record<string, { d: number }> = {}
    for (const ly of model.layers) {
      for (const ob of ly.objects) {
        if (ids.includes(ob.id)) orig[ob.id] = { d: ob.rotate }
      }
    }
    rotateGroupRef.current = { ids, orig }
  }, [selectedIds, model.layers])

  return (
    <div
      role="application"
      aria-label="WYSIWYG Canvas"
      tabIndex={0}
      onKeyDown={onKeyDown}
      onMouseDown={(e) => {
        // 빈 캔버스 클릭 시 마퀴 시작 또는 선택 해제
        // 객체 onMouseDown에서 선택을 처리하므로, 여기서는 드래그 박스 시작만 담당
        if ((e.target as HTMLElement).dataset.testid === 'wysiwyg-canvas') {
          setSelectedIds([])
          setMarquee({ active: true, x0: e.clientX, y0: e.clientY, x1: e.clientX, y1: e.clientY })
          try { onSelectIds?.([]) } catch {}
        }
      }}
      onMouseMove={onMouseMoveCanvas}
      onMouseUp={onMouseUpCanvas}
      style={{ position: 'relative', width: model.width, height: model.height, outline: '0' }}
      data-testid="wysiwyg-canvas"
    >
      {enableGuides && (showGuides || isDragging) && (
        <>
          <div data-testid="guide-x" style={{ position: 'absolute', left: guidePos.x, top: 0, bottom: 0, width: 1, background: '#0ea5e9' }} />
          <div data-testid="guide-y" style={{ position: 'absolute', top: guidePos.y, left: 0, right: 0, height: 1, background: '#0ea5e9' }} />
        </>
      )}
      <div role="status" aria-live="polite" data-testid="aria-live" style={{ position: 'absolute', left: -9999, top: 'auto', width: 1, height: 1, overflow: 'hidden' }}>{ariaLiveText}</div>
      {model.layers.map((layer) => layer.visible !== false && layer.objects.map((o) => (
        <div
          key={o.id}
          data-testid={`obj-${o.id}`}
          aria-label={`object ${o.id}`}
          onMouseDown={(e) => onMouseDownObj(e, o)}
          ref={selectedId === o.id ? (el) => { selectedElRef.current = el } : undefined}
          style={{ position: 'absolute', left: o.x, top: o.y, width: o.width, height: o.height, transform: `rotate(${o.rotate}deg)`, outline: selectedIds.includes(o.id)? '2px solid #0ea5e9':'none', border: '1px solid rgba(148,163,184,0.6)', background: 'rgba(15,23,42,0.8)', color: '#f9fafb', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 4, boxSizing: 'border-box' }}
        >
          {/* 실제 오브젝트 콘텐츠 렌더링 */}
          {o.type === 'text' && (
            <p style={{ margin: 0, fontSize: o.props.fontSize, color: o.props.color }}>{o.props.text}</p>
          )}
          {o.type === 'image' && (
            <img src={o.props.src} alt={o.props.alt || ''} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'cover' }} />
          )}
          {o.type === 'button' && (
            <button
              type="button"
              style={{
                borderRadius: 9999,
                border: 'none',
                padding: '4px 12px',
                fontSize: 12,
                cursor: 'pointer',
                background: o.props.background ?? '#2563eb',
                color: o.props.color ?? '#ffffff',
                whiteSpace: 'nowrap',
              }}
            >
              {o.props.label}
            </button>
          )}
          {o.type === 'input' && (() => {
            const cid = `${o.id}-control`
            const helpId = o.props.help ? `${cid}-help` : ''
            return (
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ color: '#e5e7eb', fontSize: 12 }}>{o.props.label}</span>
                <input
                  id={cid}
                  name={o.props.name}
                  placeholder={o.props.placeholder ?? ''}
                  required={!!o.props.required}
                  aria-describedby={helpId || undefined}
                  style={{ width: '100%', height: 28, padding: '4px 8px', border: '1px solid #94a3b8', background: '#0f172a', color: '#f9fafb' }}
                />
                {o.props.help ? <small id={helpId} style={{ color: '#94a3b8', fontSize: 11 }}>{o.props.help}</small> : null}
              </label>
            )
          })()}
          {o.type === 'select' && (() => {
            const cid = `${o.id}-control`
            const helpId = o.props.help ? `${cid}-help` : ''
            return (
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ color: '#e5e7eb', fontSize: 12 }}>{o.props.label}</span>
                <select
                  id={cid}
                  name={o.props.name}
                  value={o.props.defaultValue ?? ''}
                  aria-describedby={helpId || undefined}
                  style={{ width: '100%', height: 28, padding: '4px 8px', border: '1px solid #94a3b8', background: '#0f172a', color: '#f9fafb' }}
                  onChange={() => { /* no-op in canvas preview */ }}
                >
                  {(o.props.options ?? []).map((opt: string) => (
                    <option key={opt} value={opt}>{opt}</option>
                  ))}
                </select>
                {o.props.help ? <small id={helpId} style={{ color: '#94a3b8', fontSize: 11 }}>{o.props.help}</small> : null}
              </label>
            )
          })()}
          {o.type === 'textarea' && (() => {
            const cid = `${o.id}-control`
            const helpId = o.props.help ? `${cid}-help` : ''
            return (
              <label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <span style={{ color: '#e5e7eb', fontSize: 12 }}>{o.props.label}</span>
                <textarea
                  id={cid}
                  name={o.props.name}
                  placeholder={o.props.placeholder ?? ''}
                  required={!!o.props.required}
                  rows={o.props.rows ?? 3}
                  aria-describedby={helpId || undefined}
                  style={{ width: '100%', padding: '4px 8px', border: '1px solid #94a3b8', background: '#0f172a', color: '#f9fafb', resize: 'none' }}
                />
                {o.props.help ? <small id={helpId} style={{ color: '#94a3b8', fontSize: 11 }}>{o.props.help}</small> : null}
              </label>
            )
          })()}
          {o.type === 'radio' && (
            <fieldset style={{ display: 'flex', flexDirection: 'column', gap: 4, border: '1px solid transparent' }}>
              <legend style={{ color: '#e5e7eb', fontSize: 12 }}>{o.props.legend}</legend>
              {(o.props.options ?? []).map((opt: string) => (
                <label key={opt} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <input type="radio" name={o.props.name} value={opt} checked={(o.props.defaultValue ?? '') === opt} onChange={() => {}} required={!!o.props.required} />
                  <span>{opt}</span>
                </label>
              ))}
            </fieldset>
          )}
          {o.type === 'checkbox' && (
            <fieldset style={{ display: 'flex', flexDirection: 'column', gap: 4, border: '1px solid transparent' }}>
              <legend style={{ color: '#e5e7eb', fontSize: 12 }}>{o.props.legend}</legend>
              {(o.props.options ?? []).map((opt: string) => (
                <label key={opt} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <input type="checkbox" name={o.props.name} value={opt} checked={(o.props.defaultValues ?? []).includes(opt)} onChange={() => {}} required={!!o.props.required} />
                  <span>{opt}</span>
                </label>
              ))}
            </fieldset>
          )}
          {selectedIds.includes(o.id) && (
            <>
              {/* West (left-middle) */}
              <div
                data-testid={`resize-handle-w-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'w')}
                style={{ position: 'absolute', left: -5, top: '50%', marginTop: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'ew-resize' }}
              />
              {/* North (top-middle) */}
              <div
                data-testid={`resize-handle-n-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'n')}
                style={{ position: 'absolute', left: '50%', top: -5, marginLeft: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'ns-resize' }}
              />
              {/* East (right-middle) */}
              <div
                data-testid={`resize-handle-e-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'e')}
                style={{ position: 'absolute', right: -5, top: '50%', marginTop: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'ew-resize' }}
              />
              {/* South (bottom-middle) */}
              <div
                data-testid={`resize-handle-s-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 's')}
                style={{ position: 'absolute', left: '50%', bottom: -5, marginLeft: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'ns-resize' }}
              />
              {/* North-East (top-right corner) */}
              <div
                data-testid={`resize-handle-ne-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'ne')}
                style={{ position: 'absolute', right: -5, top: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'nesw-resize' }}
              />
              {/* North-West (top-left corner) */}
              <div
                data-testid={`resize-handle-nw-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'nw')}
                style={{ position: 'absolute', left: -5, top: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'nwse-resize' }}
              />
              {/* South-East (bottom-right corner) - legacy testid alias */}
              <div
                data-testid={`resize-handle-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'se')}
                style={{ position: 'absolute', right: -5, bottom: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'nwse-resize' }}
              />
              <div
                data-testid={`resize-handle-se-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'se')}
                style={{ position: 'absolute', right: -5, bottom: -5, width: 10, height: 10, background: 'transparent', cursor: 'nwse-resize' }}
              />
              {/* South-West (bottom-left corner) */}
              <div
                data-testid={`resize-handle-sw-${o.id}`}
                onMouseDown={(e) => onMouseDownResize(e, o, 'sw')}
                style={{ position: 'absolute', left: -5, bottom: -5, width: 10, height: 10, background: '#0ea5e9', cursor: 'nesw-resize' }}
              />
            </>
          )}
          {selectedIds.includes(o.id) && (
            <div
              data-testid={`rotate-handle-${o.id}`}
              onMouseDown={(e) => onMouseDownRotate(e, o)}
              style={{ position: 'absolute', left: '50%', top: -16, width: 10, height: 10, marginLeft: -5, background: '#0ea5e9', borderRadius: 5, cursor: 'grab' }}
            />
          )}
        </div>
      )))}
      {/* Moveable 최소 통합: 드래그/리사이즈/회전 + 8px 스냅 그리드 활성화 */}
      <Moveable
        target={selectedId ? `[data-testid="obj-${selectedId}"]` : undefined}
        draggable={selectedIds.length === 1}
        rotatable={selectedIds.length === 1}
        snappable
        snapGridWidth={8}
        snapGridHeight={8}
        onDragStart={() => {
          if (resizingRef.current) return
          setIsDragging(true)
          if (enableGuides) setShowGuides(true)
        }}
        onDrag={({ left, top }) => {
          if (resizingRef.current) return
          if (!selected) return
          const rawLeftInput = typeof left === 'number' ? left : 0
          const rawTopInput = typeof top === 'number' ? top : 0
          // 다중 선택 시: 그룹 bbox 기준으로 스냅 계산
          let baseLeft = selected.x
          let baseTop = selected.y
          let rawLeft = rawLeftInput
          let rawTop = rawTopInput
          let groupLeft = selected.x
          let groupTop = selected.y
          let groupWidth = selected.width
          let groupHeight = selected.height
          if (selectedIds.length > 1) {
            let minL = Infinity, minT = Infinity, maxR = -Infinity, maxB = -Infinity
            for (const ly of model.layers) {
              for (const o of ly.objects) {
                if (!selectedIds.includes(o.id)) continue
                minL = Math.min(minL, o.x)
                minT = Math.min(minT, o.y)
                maxR = Math.max(maxR, o.x + o.width)
                maxB = Math.max(maxB, o.y + o.height)
              }
            }
            groupLeft = minL
            groupTop = minT
            groupWidth = Math.max(0, maxR - minL)
            groupHeight = Math.max(0, maxB - minT)
            const dxProp = rawLeftInput - selected.x
            const dyProp = rawTopInput - selected.y
            baseLeft = groupLeft
            baseTop = groupTop
            rawLeft = groupLeft + dxProp
            rawTop = groupTop + dyProp
          }
          let nx = snap8(rawLeft)
          let ny = snap8(rawTop)

          // 객체-객체 스냅(수직/수평 중심 + 에지): 임계 8px
          const threshold = 8
          let snappedCenterX: number | undefined
          let snappedCenterY: number | undefined
          let centerDistX = Number.POSITIVE_INFINITY
          let centerDistY = Number.POSITIVE_INFINITY
          // 에지 스냅 후보(nx, ny로 바로 설정 가능한 값) 및 가이드 좌표
          let edgeNx: number | undefined
          let edgeNy: number | undefined
          let guideEdgeX: number | undefined
          let guideEdgeY: number | undefined
          let minDistX = Number.POSITIVE_INFINITY
          let minDistY = Number.POSITIVE_INFINITY
          for (const layer of model.layers) {
            for (const obj of layer.objects) {
              if (selectedIds.includes(obj.id)) continue
              const otherCenterX = obj.x + obj.width / 2
              const selCenterX = rawLeft + (selectedIds.length > 1 ? groupWidth : selected.width) / 2
              const dxCenter = Math.abs(selCenterX - otherCenterX)
              if (dxCenter <= threshold && dxCenter < centerDistX) {
                centerDistX = dxCenter
                snappedCenterX = otherCenterX
              }
              const otherCenterY2 = obj.y + obj.height / 2
              const selCenterY2 = rawTop + (selectedIds.length > 1 ? groupHeight : selected.height) / 2
              const dyCenter = Math.abs(selCenterY2 - otherCenterY2)
              if (dyCenter <= threshold && dyCenter < centerDistY) {
                centerDistY = dyCenter
                snappedCenterY = otherCenterY2
              }

              // 에지 스냅 후보 계산 (X축)
              const selLeft = rawLeft
              const selRight = rawLeft + (selectedIds.length > 1 ? groupWidth : selected.width)
              const otherLeft = obj.x
              const otherRight = obj.x + obj.width
              const xPairs: Array<{dist: number; nx: number; guideX: number}> = [
                { dist: Math.abs(selLeft - otherLeft), nx: otherLeft, guideX: otherLeft },      // L-L
                { dist: Math.abs(selLeft - otherRight), nx: otherRight, guideX: otherRight },  // L-R
                { dist: Math.abs(selRight - otherLeft), nx: otherLeft - (selectedIds.length > 1 ? groupWidth : selected.width), guideX: otherLeft }, // R-L
                { dist: Math.abs(selRight - otherRight), nx: otherRight - (selectedIds.length > 1 ? groupWidth : selected.width), guideX: otherRight }, // R-R
              ]
              for (const p of xPairs) {
                if (p.dist <= threshold && p.dist < minDistX) {
                  minDistX = p.dist
                  edgeNx = p.nx
                  guideEdgeX = p.guideX
                }
              }

              // 에지 스냅 후보 계산 (Y축)
              const selTop = rawTop
              const selBottom = rawTop + (selectedIds.length > 1 ? groupHeight : selected.height)
              const otherTop = obj.y
              const otherBottom = obj.y + obj.height
              const yPairs: Array<{dist: number; ny: number; guideY: number}> = [
                { dist: Math.abs(selTop - otherTop), ny: otherTop, guideY: otherTop },      // T-T
                { dist: Math.abs(selTop - otherBottom), ny: otherBottom, guideY: otherBottom }, // T-B
                { dist: Math.abs(selBottom - otherTop), ny: otherTop - (selectedIds.length > 1 ? groupHeight : selected.height), guideY: otherTop }, // B-T
                { dist: Math.abs(selBottom - otherBottom), ny: otherBottom - (selectedIds.length > 1 ? groupHeight : selected.height), guideY: otherBottom }, // B-B
              ]
              for (const p of yPairs) {
                if (p.dist <= threshold && p.dist < minDistY) {
                  minDistY = p.dist
                  edgeNy = p.ny
                  guideEdgeY = p.guideY
                }
              }
            }
          }

          // 우선순위: 에지 스냅 > 중심 스냅 > 그리드
          if (typeof edgeNx === 'number') {
            nx = edgeNx
          } else if (typeof snappedCenterX === 'number') {
            nx = snappedCenterX - (selectedIds.length > 1 ? groupWidth : selected.width) / 2
          }
          if (typeof edgeNy === 'number') {
            ny = edgeNy
          } else if (typeof snappedCenterY === 'number') {
            ny = snappedCenterY - (selectedIds.length > 1 ? groupHeight : selected.height) / 2
          }

          // 가이드 좌표 업데이트(에지 가이드 우선, 없으면 중심, 최후에는 nx/ny)
          const gx = typeof guideEdgeX === 'number' ? guideEdgeX : (typeof snappedCenterX === 'number' ? snappedCenterX : nx)
          const gy = typeof guideEdgeY === 'number' ? guideEdgeY : (typeof snappedCenterY === 'number' ? snappedCenterY : ny)
          if (enableGuides) setShowGuides(true)
          setGuidePos({ x: gx, y: gy })

          if (selectedIds.length > 1) {
            const dx = nx - baseLeft
            const dy = ny - baseTop
            updateObjects(selectedIds, (o) => ({ ...o, x: o.x + dx, y: o.y + dy }))
          } else {
            updateObject(selected.id, (o) => ({ ...o, x: nx, y: ny }))
          }
        }}
        onDragEnd={() => {
          setIsDragging(false)
          setShowGuides(false)
        }}
        onRotate={({ rotate, inputEvent }) => {
          if (!selected) return
          const raw = typeof rotate === 'number' ? rotate : 0
          const shift = !!(inputEvent && (inputEvent as { shiftKey?: boolean }).shiftKey)
          const step = shift ? 1 : 15
          const snapped = Math.sign(raw) * Math.ceil(Math.abs(raw) / step) * step
          if (selectedIds.length > 1 && rotateGroupRef.current) {
            const { ids } = rotateGroupRef.current
            updateObjects(ids, (o) => ({ ...o, rotate: snapped }))
          } else {
            updateObject(selected.id, (o) => ({ ...o, rotate: snapped }))
          }
        }}
      />
    </div>
  )
}
