import React, { useCallback, useMemo, useRef, useState } from 'react'
import Moveable from 'react-moveable'
import type { Frame, WObject } from '@/lib/wysiwyg/schema'
import { snapAngle as snapAngleUtil } from '@/lib/wysiwyg/snap'

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

// Shift 키 타입 가드: unknown 입력에서 shiftKey 존재 여부를 안전하게 판별
function isShiftEvent(ev: unknown): ev is { shiftKey?: boolean } {
  return typeof ev === 'object' && ev !== null && 'shiftKey' in (ev as Record<string, unknown>)
}

// 테스트 환경(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) => null
  }
}

export type CanvasProps = {
  frame: Frame
  onChange?: (next: Frame) => void
}

export function Canvas({ frame, onChange }: CanvasProps) {
  const [model, setModel] = useState<Frame>(frame)
  const [selectedId, setSelectedId] = useState<string | null>(model.layers[0]?.objects[0]?.id ?? null)
  const draggingRef = useRef<{ id: string; startX: number; startY: number; origX: number; origY: number } | null>(null)
  const resizingRef = useRef<{ id: string; startX: number; startY: number; origW: number; origH: number } | null>(null)
  const rotatingRef = useRef<{ id: string; startX: number; startY: number; origDeg: number } | null>(null)
  const [showGuides, setShowGuides] = useState(false)
  const [guidePos, setGuidePos] = useState<{ x: number; y: number }>({ x: 0, y: 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])

  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 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) => {
    setSelectedId(o.id)
    draggingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origX: o.x, origY: o.y }
    setShowGuides(true)
    setGuidePos({ x: snap8(o.x), y: snap8(o.y) })
  }, [])

  const onMouseMoveCanvas = useCallback((e: React.MouseEvent) => {
    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
      }
      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
      const nw = Math.max(8, snap8(r.origW + dx))
      const nh = Math.max(8, snap8(r.origH + dy))
      updateObject(r.id, (o) => ({ ...o, width: nw, height: nh }))
      return
    }
    const d = draggingRef.current
    if (d) {
      const dx = e.clientX - d.startX
      const dy = e.clientY - d.startY
      const nx = snap8(d.origX + dx)
      const ny = snap8(d.origY + dy)
      setGuidePos({ x: nx, y: ny })
      updateObject(d.id, (o) => ({ ...o, x: nx, y: ny }))
    }
  }, [updateObject])

  const onMouseUpCanvas = useCallback(() => {
    draggingRef.current = null
    resizingRef.current = null
    rotatingRef.current = null
    setShowGuides(false)
  }, [])

  const onMouseDownResize = useCallback((e: React.MouseEvent, o: WObject) => {
    e.stopPropagation()
    resizingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origW: o.width, origH: o.height }
  }, [])

  const onMouseDownRotate = useCallback((e: React.MouseEvent, o: WObject) => {
    e.stopPropagation()
    rotatingRef.current = { id: o.id, startX: e.clientX, startY: e.clientY, origDeg: o.rotate }
  }, [])

  return (
    <div
      role="application"
      aria-label="WYSIWYG Canvas"
      tabIndex={0}
      onKeyDown={onKeyDown}
      onMouseMove={onMouseMoveCanvas}
      onMouseUp={onMouseUpCanvas}
      style={{ position: 'relative', width: model.width, height: model.height, outline: '0' }}
      data-testid="wysiwyg-canvas"
    >
      {showGuides && (
        <>
          <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' }} />
        </>
      )}
      {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: selectedId===o.id? '2px solid #0ea5e9':'none' }}
        >
          {selectedId === o.id && (
            <div
              data-testid={`resize-handle-${o.id}`}
              onMouseDown={(e) => onMouseDownResize(e, o)}
              style={{ position: 'absolute', right: 0, bottom: 0, width: 10, height: 10, background: '#0ea5e9', cursor: 'nwse-resize' }}
            />
          )}
          {selectedId === 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
        resizable
        rotatable
        snappable
        snapGridWidth={8}
        snapGridHeight={8}
        onDragStart={() => {
          // 드래그 시작 시 가이드 표시
          setShowGuides(true)
        }}
        onDrag={({ left, top }) => {
          if (!selected) return
          const rawLeft = typeof left === 'number' ? left : 0
          const rawTop = typeof top === 'number' ? top : 0
          let nx = snap8(rawLeft)
          let ny = snap8(rawTop)

          // 객체-객체 스냅(수직/수평 중심 + 에지): 임계 8px
          const threshold = 8
          let snappedCenterX: number | undefined
          let snappedCenterY: number | undefined
          // 에지 스냅 후보(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
          outer: for (const layer of model.layers) {
            for (const obj of layer.objects) {
              if (obj.id === selected.id) continue
              const otherCenterX = obj.x + obj.width / 2
              const selCenterX = rawLeft + selected.width / 2
              if (Math.abs(selCenterX - otherCenterX) <= threshold) {
                snappedCenterX = otherCenterX
                // 수평 중심도 동시에 체크
                const otherCenterY = obj.y + obj.height / 2
                const selCenterY = rawTop + selected.height / 2
                if (Math.abs(selCenterY - otherCenterY) <= threshold) {
                  snappedCenterY = otherCenterY
                }
                break outer
              }
              const otherCenterY2 = obj.y + obj.height / 2
              const selCenterY2 = rawTop + selected.height / 2
              if (Math.abs(selCenterY2 - otherCenterY2) <= threshold) {
                snappedCenterY = otherCenterY2
                // x는 그리드, y만 스냅으로 나가고 루프 종료
                break outer
              }

              // 에지 스냅 후보 계산 (X축)
              const selLeft = rawLeft
              const selRight = rawLeft + 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 - selected.width, guideX: otherLeft }, // R-L
                { dist: Math.abs(selRight - otherRight), nx: otherRight - 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 + 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 - selected.height, guideY: otherTop }, // B-T
                { dist: Math.abs(selBottom - otherBottom), ny: otherBottom - 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 - selected.width / 2
          }
          if (typeof edgeNy === 'number') {
            ny = edgeNy
          } else if (typeof snappedCenterY === 'number') {
            ny = snappedCenterY - selected.height / 2
          }

          // 가이드라인 좌표 업데이트: 축별로 에지/중심/그리드 순으로 우선 표시
          const guideX = typeof guideEdgeX === 'number' ? guideEdgeX : (typeof snappedCenterX === 'number' ? snappedCenterX : nx)
          const guideY = typeof guideEdgeY === 'number' ? guideEdgeY : (typeof snappedCenterY === 'number' ? snappedCenterY : ny)
          setGuidePos({ x: guideX, y: guideY })

          updateObject(selected.id, (o) => ({ ...o, x: nx, y: ny }))
        }}
        onDragEnd={() => {
          // 드래그 종료 시 가이드 숨김
          setShowGuides(false)
        }}
        onResize={({ width, height }) => {
          if (!selected) return
          const nw = Math.max(8, snap8(typeof width === 'number' ? width : 0))
          const nh = Math.max(8, snap8(typeof height === 'number' ? height : 0))
          updateObject(selected.id, (o) => ({ ...o, width: nw, height: nh }))
        }}
        onRotate={({ rotate, inputEvent }) => {
          if (!selected) return
          const fine = isShiftEvent(inputEvent) && !!inputEvent.shiftKey
          const nd = snapAngleUtil(typeof rotate === 'number' ? rotate : 0, fine)
          updateObject(selected.id, (o) => ({ ...o, rotate: nd }))
        }}
      />
    </div>
  )
}
