"use client";

import type { CSSProperties, ReactNode } from "react";
import { Fragment, useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FilePlus2, Trash2, Eye, Pencil, ListChecks, Undo2, Redo2 } from "lucide-react";
import {
  DndContext,
  PointerSensor,
  closestCenter,
  rectIntersection,
  useSensor,
  useSensors,
  DragEndEvent,
  DragStartEvent,
  DragOverlay,
  useDroppable,
  useDndContext,
} from "@dnd-kit/core";
import {
  SortableContext,
  verticalListSortingStrategy,
  useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useEditorStore } from "@/features/editor/state/editorStore";
import type {
  Block,
  TextBlockProps,
  ButtonBlockProps,
  ImageBlockProps,
  VideoBlockProps,
  SectionBlockProps,
  DividerBlockProps,
  ListBlockProps,
  FormBlockProps,
  FormFieldConfig,
  FormInputBlockProps,
  FormSelectBlockProps,
  FormCheckboxBlockProps,
  FormRadioBlockProps,
  ListItemNode,
  ProjectConfig,
} from "@/features/editor/state/editorStore";
import {
  normalizeVideoSourceUrl,
  resolveVideoPlatform,
  buildVideoEmbedUrl,
  computeVideoEditorTokens,
} from "@/features/editor/utils/videoHelpers";
import { computeTextEditorTokens } from "@/features/editor/utils/textHelpers";
import { computeButtonPbTokens, computeButtonEditorTokens } from "@/features/editor/utils/buttonHelpers";
import { computeDividerEditorTokens } from "@/features/editor/utils/dividerHelpers";
import { computeListEditorTokens } from "@/features/editor/utils/listHelpers";
import { computeImageEditorTokens } from "@/features/editor/utils/imageHelpers";
import { computeSectionEditorTokens } from "@/features/editor/utils/sectionHelpers";
import {
  computeFormInputEditorTokens,
  computeFormSelectEditorTokens,
  computeFormOptionGroupEditorTokens,
} from "@/features/editor/utils/formHelpers";
import { ButtonPropertiesPanel } from "./panels/ButtonPropertiesPanel";
import { TextPropertiesPanel } from "./panels/TextPropertiesPanel";
import { ListPropertiesPanel } from "./panels/ListPropertiesPanel";
import { DividerPropertiesPanel } from "./panels/DividerPropertiesPanel";
import { ImagePropertiesPanel } from "./panels/ImagePropertiesPanel";
import { SectionPropertiesPanel } from "./panels/SectionPropertiesPanel";
import { BlocksSidebar } from "./panels/BlocksSidebar";
import { PropertiesSidebar } from "./panels/PropertiesSidebar";
import { EditorCanvas } from "./EditorCanvas";

export default function EditorPage() {
  const router = useRouter();
  const blocks = useEditorStore((state) => state.blocks);
  const selectedBlockId = useEditorStore((state) => state.selectedBlockId);
  const selectedListItemId = useEditorStore((state) => (state as any).selectedListItemId as string | null | undefined);
  const addTextBlock = useEditorStore((state) => state.addTextBlock);
  const addButtonBlock = useEditorStore((state) => state.addButtonBlock);
  const addImageBlock = useEditorStore((state) => state.addImageBlock);
  const addDividerBlock = useEditorStore((state) => state.addDividerBlock);
  const addListBlock = useEditorStore((state) => state.addListBlock);
  const addSectionBlock = useEditorStore((state) => state.addSectionBlock);
  const addFormBlock = useEditorStore((state) => state.addFormBlock);
  const addFormInputBlock = useEditorStore((state) => (state as any).addFormInputBlock);
  const addFormSelectBlock = useEditorStore((state) => (state as any).addFormSelectBlock);
  const addFormCheckboxBlock = useEditorStore((state) => (state as any).addFormCheckboxBlock);
  const addFormRadioBlock = useEditorStore((state) => (state as any).addFormRadioBlock);
  const addHeroTemplateSection = useEditorStore((state) => state.addHeroTemplateSection);
  const addFeaturesTemplateSection = useEditorStore((state) => state.addFeaturesTemplateSection);
  const addCtaTemplateSection = useEditorStore((state) => state.addCtaTemplateSection);
  const addFaqTemplateSection = useEditorStore((state) => state.addFaqTemplateSection);
  const addPricingTemplateSection = useEditorStore((state) => state.addPricingTemplateSection);
  const addTestimonialsTemplateSection = useEditorStore((state) => state.addTestimonialsTemplateSection);
  const addBlogTemplateSection = useEditorStore((state) => state.addBlogTemplateSection);
  const addTeamTemplateSection = useEditorStore((state) => state.addTeamTemplateSection);
  const addFooterTemplateSection = useEditorStore((state) => state.addFooterTemplateSection);
  const updateBlock = useEditorStore((state) => state.updateBlock);
  const selectBlock = useEditorStore((state) => state.selectBlock);
  const selectListItem = useEditorStore((state) => (state as any).selectListItem as (id: string | null) => void);
  const indentSelectedListItem = useEditorStore(
    (state) => (state as any).indentSelectedListItem as (blockId: string) => void,
  );
  const outdentSelectedListItem = useEditorStore(
    (state) => (state as any).outdentSelectedListItem as (blockId: string) => void,
  );
  const moveSelectedListItemUp = useEditorStore(
    (state) => (state as any).moveSelectedListItemUp as (blockId: string) => void,
  );
  const moveSelectedListItemDown = useEditorStore(
    (state) => (state as any).moveSelectedListItemDown as (blockId: string) => void,
  );
  const replaceBlocks = useEditorStore((state) => state.replaceBlocks);
  const reorderBlocks = useEditorStore((state) => state.reorderBlocks);
  const moveBlock = useEditorStore((state) => state.moveBlock);
  const undo = useEditorStore((state) => state.undo);
  const redo = useEditorStore((state) => state.redo);
  const removeBlock = useEditorStore((state) => state.removeBlock);
  const duplicateBlock = useEditorStore((state) => state.duplicateBlock);
  const projectConfig = useEditorStore(
	  (state) => (state as any).projectConfig as ProjectConfig,
	);
  const updateProjectConfig = useEditorStore((state) => state.updateProjectConfig);

  const [editingBlockId, setEditingBlockId] = useState<string | null>(null);
  const [editingText, setEditingText] = useState("");

  // 드래그 중인 블록 ID를 추적하여 DragOverlay에 일관된 고스트 카드를 렌더링한다.
  const [activeDragId, setActiveDragId] = useState<string | null>(null);

  const [exportJson, setExportJson] = useState("");
  const [importJson, setImportJson] = useState("");

  const [projectMessage, setProjectMessage] = useState("");
  // 멀티 선택(MVP)을 위한 로컬 선택 상태: 단일 선택(selectedBlockId)과 병행 사용한다.
  const [selectedBlockIds, setSelectedBlockIds] = useState<string[]>([]);

  const historyLength = useEditorStore((state) => (state as any).history?.length ?? 0);
  const futureLength = useEditorStore((state) => (state as any).future?.length ?? 0);
  const canUndo = historyLength > 0;
  const canRedo = futureLength > 0;

  useEffect(() => {
    let cancelled = false;

    const checkAuth = async () => {
      try {
        const res = await fetch("/api/auth/me");
        if (!res.ok && res.status === 401 && !cancelled) {
          router.push("/login");
        }
      } catch {
        // 네트워크 오류 등은 일단 무시하고, 페이지 접근을 막지 않는다.
      }
    };

    void checkAuth();

    return () => {
      cancelled = true;
    };
  }, [router]);

  const [menuOpen, setMenuOpen] = useState(false);
  const [activeModal, setActiveModal] = useState<"project" | "json" | null>(null);

  const sensors = useSensors(useSensor(PointerSensor));

  const startEditing = (id: string, initialText: string) => {
    selectBlock(id);
    setEditingBlockId(id);
    setEditingText(initialText);
  };

  const handleExportZip = async () => {
    try {
      const payload = {
        blocks,
        projectConfig,
      };

      const response = await fetch("/api/export", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        const text = await response.text().catch(() => "");
        const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
        console.error(
          "정적 파일 내보내기 실패",
          response.status,
          response.statusText,
          truncated,
        );
        return;
      }

      const blob = await response.blob();
      const url = URL.createObjectURL(blob);
      const slug = (projectConfig.slug ?? "").trim() || "page-builder-export";

      const link = document.createElement("a");
      link.href = url;
      link.download = `${slug}.zip`;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
    } catch (error) {
      console.error("정적 파일 내보내기 중 오류", error);
    }
  };

  const commitEditing = () => {
    if (!editingBlockId) return;
    updateBlock(editingBlockId, { text: editingText });
    setEditingBlockId(null);
  };

  const cancelEditing = () => {
    if (!editingBlockId) return;
    const current = blocks.find((b) => b.id === editingBlockId);
    if (current && current.type === "text") {
      const textProps = current.props as TextBlockProps;
      setEditingText(textProps.text);
    }
    setEditingBlockId(null);
  };

  const handleExportJson = () => {
    try {
      const snapshot = {
        blocks,
        projectConfig,
      };
      const payload = JSON.stringify(snapshot, null, 2);
      setExportJson(payload);
      setImportJson(payload);
    } catch {
      // JSON 직렬화 실패 시는 조용히 무시 (순수 클라이언트 상태라 치명적이지 않음)
    }
  };

  const handleClearCanvas = () => {
    replaceBlocks([]);
    setExportJson("");
    setImportJson("");
  };

  const handleApplyImportJson = () => {
    if (!importJson.trim()) return;
    try {
      const parsed = JSON.parse(importJson) as {
        blocks?: Block[];
        projectConfig?: ProjectConfig;
      };

      if (!parsed || typeof parsed !== "object") return;

      if (!Array.isArray(parsed.blocks)) return;

      replaceBlocks(parsed.blocks as any);

      if (parsed.projectConfig && typeof parsed.projectConfig === "object") {
        updateProjectConfig(parsed.projectConfig as ProjectConfig);
      }

      setExportJson(JSON.stringify(parsed, null, 2));
    } catch {
      // 파싱 에러는 무시 (추후에는 토스트 등으로 피드백 가능)
    }
  };

  const handleSaveProject = async () => {
    const slug = (projectConfig.slug ?? "").trim();
    if (!slug) {
      setProjectMessage("프로젝트 주소를 입력해 주세요.");
      return;
    }

    const title = (projectConfig.title ?? "").trim() || "제목 없음";

    try {
      // 1) 로컬스토리지에 현재 상태 저장 (슬러그 기준)
      if (typeof window !== "undefined") {
        const localKey = `pb:project:${slug}`;
        const payload = {
          title,
          contentJson: blocks,
        };
        window.localStorage.setItem(localKey, JSON.stringify(payload));
      }

      // 2) 서버에도 저장 (백업/공유용)
      const response = await fetch("/api/projects", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          title,
          slug,
          contentJson: blocks,
        }),
      });

      if (!response.ok) {
        if (response.status === 409) {
          try {
            const data = await response.json();
            if (data && typeof (data as any).message === "string") {
              setProjectMessage((data as any).message as string);
              return;
            }
          } catch {
          }
        }

        setProjectMessage("프로젝트 저장에 실패했습니다.");
        return;
      }

      const data = await response.json();
      setProjectMessage(`프로젝트가 저장되었습니다: ${data.slug}`);

      if (typeof window !== "undefined") {
        window.location.href = "/projects";
      }
    } catch (error) {
      console.error(error);
      setProjectMessage("프로젝트 저장 중 오류가 발생했습니다.");
    }
  };

  const handleLoadProject = async () => {
    const slug = (projectConfig.slug ?? "").trim();
    if (!slug) {
      setProjectMessage("불러올 주소를 입력해 주세요.");
      return;
    }

    try {
      // 1) 로컬스토리지에서 먼저 조회
      if (typeof window !== "undefined") {
        const localKey = `pb:project:${slug}`;
        const raw = window.localStorage.getItem(localKey);
        if (raw) {
          try {
            const parsed = JSON.parse(raw);
            if (parsed && Array.isArray(parsed.contentJson)) {
              replaceBlocks(parsed.contentJson as any);
              setProjectMessage(`로컬에서 프로젝트를 불러왔습니다: ${slug}`);
              return;
            }
          } catch {
            // 로컬 파싱 에러는 무시하고 서버 시도
          }
        }
      }

      // 2) 로컬에 없으면 서버에서 조회
      const response = await fetch(`/api/projects/${slug}`);
      if (!response.ok) {
        setProjectMessage("프로젝트를 불러오지 못했습니다.");
        return;
      }

      const data = await response.json();
      if (data && Array.isArray(data.contentJson)) {
        replaceBlocks(data.contentJson as any);
        setProjectMessage(`프로젝트를 불러왔습니다: ${slug}`);
      } else {
        setProjectMessage("프로젝트 데이터 형식이 올바르지 않습니다.");
      }
    } catch (error) {
      console.error(error);
      setProjectMessage("프로젝트 불러오기 중 오류가 발생했습니다.");
    }
  };

  const handleDeleteProject = async () => {
    const slug = (projectConfig.slug ?? "").trim();
    if (!slug) {
      setProjectMessage("삭제할 프로젝트 주소가 없습니다.");
      return;
    }

    if (typeof window === "undefined") return;

    const confirmed = window.confirm(
      "현재 프로젝트를 삭제할까요? 삭제 후에는 되돌릴 수 없습니다. (로컬 저장 및 자동저장 데이터도 함께 삭제됩니다.)",
    );
    if (!confirmed) return;

    try {
      const response = await fetch(`/api/projects/${encodeURIComponent(slug)}`, {
        method: "DELETE",
      });

      if (!response.ok) {
        setProjectMessage("프로젝트 삭제에 실패했습니다.");
        return;
      }

      // 프로젝트 저장 및 자동저장 스냅샷을 정리한다.
      window.localStorage.removeItem(`pb:project:${slug}`);
      window.localStorage.removeItem(`pb:autosave:${slug}`);

      setProjectMessage("프로젝트가 삭제되었습니다.");

      window.location.href = "/projects";
    } catch (error) {
      console.error(error);
      setProjectMessage("프로젝트 삭제 중 오류가 발생했습니다.");
    }
  };

  const handleDragStart = (event: DragStartEvent) => {
    const { active } = event;
    setActiveDragId(String(active.id));
  };

  const handleDragEnd = (event: DragEndEvent) => {
    // 드래그가 끝나면 고스트 카드를 숨긴다.
    setActiveDragId(null);
    const { active, over } = event;
    if (!over || active.id === over.id) return;
    const activeId = String(active.id);
    const overId = String(over.id);

    // 드롭존(블록 사이/최하단 가이드 영역)에 드롭한 경우: 컨테이너 기준으로 위치를 조정한다.
    if (overId.startsWith("dropzone-before:")) {
      const [, targetId] = overId.split(":");
      if (targetId) {
        const activeBlock = blocks.find((b) => b.id === activeId);
        const targetBlock = blocks.find((b) => b.id === targetId);

        if (!activeBlock || !targetBlock) {
          reorderBlocks(activeId, targetId);
          return;
        }

        const activeSectionId = activeBlock.sectionId ?? null;
        const activeColumnId = activeBlock.columnId ?? null;
        const targetSectionId = targetBlock.sectionId ?? null;
        const targetColumnId = targetBlock.columnId ?? null;

        // 섹션 안 블록을 루트 드롭존으로 끌어낸 경우: 루트로 이동 후 순서 재배치
        if (activeSectionId && !targetSectionId && !targetColumnId && targetBlock.type !== "section") {
          moveBlock(activeId, null, null);
          reorderBlocks(activeId, targetId);
          return;
        }

        // 같은 컨테이너(루트 또는 동일 섹션/컬럼) 내에서는 순서만 변경
        if (
          (!activeSectionId && !activeColumnId && !targetSectionId && !targetColumnId) ||
          (activeSectionId === targetSectionId && activeColumnId === targetColumnId)
        ) {
          reorderBlocks(activeId, targetId);
          return;
        }

        // 다른 섹션/컬럼으로의 이동: 컨테이너를 변경한 뒤 순서 재배치
        moveBlock(activeId, targetSectionId, targetColumnId);
        reorderBlocks(activeId, targetId);
      }
      return;
    }

    // 최하단 드롭존에 드롭한 경우: 컨테이너의 맨 아래로 이동한다.
    if (overId === "dropzone-after-root" || overId.startsWith("dropzone-after:")) {
      const activeBlock = blocks.find((b) => b.id === activeId);
      if (!activeBlock) return;

      // 루트 최하단 드롭존
      if (overId === "dropzone-after-root") {
        const rootBlocksForDrop = blocks.filter((b) => !b.sectionId);
        const lastRoot = rootBlocksForDrop[rootBlocksForDrop.length - 1];

        if (!lastRoot) {
          // 아직 루트 블록이 없다면, 섹션 안 블록을 루트로만 이동시킨다.
          if (activeBlock.sectionId || activeBlock.columnId) {
            moveBlock(activeId, null, null);
          }
          return;
        }

        // 섹션 안에서 루트 최하단으로 끌어낸 경우: 먼저 루트로 이동 후 마지막 루트 블록 뒤로 재배치
        if (activeBlock.sectionId || activeBlock.columnId) {
          moveBlock(activeId, null, null);
        }

        if (lastRoot.id !== activeId) {
          reorderBlocks(activeId, lastRoot.id);
        }

        return;
      }

      // 섹션 컬럼 최하단 드롭존: id 형식은 dropzone-after:sectionId:columnId
      const [, sectionId, columnId] = overId.split(":");
      const columnBlocks = blocks.filter(
        (b) => b.sectionId === sectionId && b.columnId === columnId,
      );
      const lastInColumn = columnBlocks[columnBlocks.length - 1];

      // 다른 컨테이너에서 이 컬럼으로 이동하는 경우: 먼저 컨테이너 이동
      if (activeBlock.sectionId !== sectionId || activeBlock.columnId !== columnId) {
        moveBlock(activeId, sectionId || null, columnId || null);
      }

      if (lastInColumn && lastInColumn.id !== activeId) {
        reorderBlocks(activeId, lastInColumn.id);
      }

      return;
    }

    // 1) 컬럼 droppable에 드롭한 경우: 해당 섹션/컬럼으로 이동만 수행한다.
    if (overId.startsWith("column:")) {
      const [, sectionId, columnId] = overId.split(":");
      moveBlock(activeId, sectionId || null, columnId || null);
      return;
    }

    const activeBlock = blocks.find((b) => b.id === activeId);
    const overBlock = blocks.find((b) => b.id === overId);

    // 2) 드롭 대상 블록을 찾지 못한 경우: 단순 순서 변경만 시도한다.
    if (!activeBlock || !overBlock) {
      reorderBlocks(activeId, overId);
      return;
    }

    const activeSectionId = activeBlock.sectionId ?? null;
    const activeColumnId = activeBlock.columnId ?? null;
    const overSectionId = overBlock.sectionId ?? null;
    const overColumnId = overBlock.columnId ?? null;

    // B-1) 루트 블록을 섹션(박스) 안으로 집어넣기:
    // 섹션 블록 위로 드랍했고, 드래그 중인 블록은 루트(섹션/컬럼이 없음)인 경우
    if (overBlock.type === "section" && !activeSectionId && !activeColumnId) {
      const sectionProps = overBlock.props as SectionBlockProps;
      const targetColumnId = sectionProps.columns?.[0]?.id ?? null;
      moveBlock(activeId, overBlock.id, targetColumnId);
      // 섹션 블록 주변에서 순서가 자연스럽게 보이도록 배열 순서도 함께 재배치
      reorderBlocks(activeId, overId);
      return;
    }

    // B-2) 섹션(박스) 안 블록을 루트로 꺼내기:
    // 드래그 중인 블록은 섹션 안에 있고, 드랍 대상은 루트 블록(섹션/컬럼이 없음)인 경우
    if (activeSectionId && !overSectionId && !overColumnId && overBlock.type !== "section") {
      moveBlock(activeId, null, null);
      reorderBlocks(activeId, overId);
      return;
    }

    // 3) 루트 블록들끼리 드래그하는 경우: 순서만 변경한다.
    if (!activeSectionId && !activeColumnId && !overSectionId && !overColumnId) {
      reorderBlocks(activeId, overId);
      return;
    }

    // 4) 같은 섹션/컬럼 내에서는 순서만 변경한다.
    if (activeSectionId === overSectionId && activeColumnId === overColumnId) {
      reorderBlocks(activeId, overId);
      return;
    }

    // 5) 다른 컬럼/섹션으로 이동: 위치를 변경한 뒤, 블록 기준 드롭이면 순서 재배치까지 진행한다.
    moveBlock(activeId, overSectionId, overColumnId);
    reorderBlocks(activeId, overId);
  };

  const handleDragCancel = () => {
    // 드래그가 취소된 경우에도 고스트 카드를 정리한다.
    setActiveDragId(null);
  };

  const rootBlocks = blocks.filter((block) => !block.sectionId);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const slug = projectConfig?.slug?.trim();
    if (!slug) return;

    const key = `pb:autosave:${slug}`;
    const raw = window.localStorage.getItem(key);
    if (!raw) return;

    try {
      const parsed = JSON.parse(raw) as { blocks?: Block[]; projectConfig?: ProjectConfig };
      if (Array.isArray(parsed.blocks)) {
        replaceBlocks(parsed.blocks as any);
      }
      if (parsed.projectConfig) {
        updateProjectConfig(parsed.projectConfig as ProjectConfig);
      }
    } catch {
      return;
    }
  }, [projectConfig?.slug, replaceBlocks, updateProjectConfig]);

  useEffect(() => {
    if (typeof window === "undefined") return;

    const slug = projectConfig?.slug?.trim();
    if (!slug) return;

    const key = `pb:autosave:${slug}`;
    const payload = {
      blocks,
      projectConfig,
    };

    try {
      window.localStorage.setItem(key, JSON.stringify(payload));
    } catch {
      return;
    }
  }, [blocks, projectConfig]);

  // 에디터 전역에서 Undo/Redo 단축키(Cmd/Ctrl+Z, Cmd/Ctrl+Shift+Z),
  // Delete/Backspace 기반 삭제, Cmd/Ctrl+D 기반 복제,
  // ArrowUp/ArrowDown을 이용한 선택 블록 이동을 처리한다.
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const target = event.target as HTMLElement | null;
      if (target) {
        const tagName = target.tagName;
        const isInputLike =
          tagName === "INPUT" ||
          tagName === "TEXTAREA" ||
          (target as HTMLElement).isContentEditable;
        if (isInputLike) {
          // 입력 포커스 상태에서는 에디터 전역 단축키를 비활성화하고,
          // 기본 브라우저 편집 동작(삭제/커서 이동 등)을 그대로 둔다.
          return;
        }
      }

      // 방향키로 선택된 블록을 위/아래로 이동 (순서 변경이 아니라 선택만 이동)
      if (event.key === "ArrowDown" || event.key === "ArrowUp") {
        const { blocks: currentBlocks, selectedBlockId: currentSelectedId, selectBlock: storeSelectBlock } =
          (useEditorStore as any).getState();

        if (!currentBlocks.length) return;

        const currentIndex = currentSelectedId
          ? currentBlocks.findIndex((b: Block) => b.id === currentSelectedId)
          : -1;

        let nextIndex = currentIndex;

        if (event.key === "ArrowDown") {
          // 선택이 없거나(자동 선택 상태를 무시) 0보다 큰 인덱스에서 시작하면 첫 번째 블록으로 이동한다.
          if (currentIndex === -1 || currentIndex > 0) {
            nextIndex = 0;
          } else {
            // 이미 0번을 선택 중이면 1번으로 한 칸 내려간다.
            nextIndex = currentBlocks.length > 1 ? 1 : 0;
          }
        } else if (event.key === "ArrowUp") {
          // 선택이 없을 때는 마지막 블록을 선택한다.
          if (currentIndex === -1) {
            nextIndex = currentBlocks.length - 1;
          } else {
            nextIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex;
          }
        }

        if (nextIndex !== -1 && nextIndex !== currentIndex) {
          event.preventDefault();
          const nextId = currentBlocks[nextIndex]?.id;
          if (nextId) {
            storeSelectBlock(nextId);
          }
        }

        return;
      }

      const isMac = navigator.platform.toLowerCase().includes("mac");
      const metaKey = isMac ? event.metaKey : event.ctrlKey;

      // Delete / Backspace 로 선택된 블록 삭제 (멀티 선택 우선)
      if (event.key === "Delete" || event.key === "Backspace") {
        const { selectedBlockId: currentSelectedId, blocks: currentBlocks } = (useEditorStore as any).getState();

        // 멀티 선택이 존재하면 멀티 삭제를 우선 처리한다.
        if (selectedBlockIds.length > 1) {
          event.preventDefault();
          const selectedSet = new Set(selectedBlockIds);
          const nextBlocks = (currentBlocks as Block[]).filter((b) => !selectedSet.has(b.id));
          replaceBlocks(nextBlocks);
          return;
        }

        // 단일 선택만 있는 경우 기존 removeBlock 로직 사용
        if (currentSelectedId) {
          event.preventDefault();
          removeBlock(currentSelectedId);
        }
        return;
      }

      // Cmd/Ctrl 없는 경우에는 여기서 종료한다.
      if (!metaKey) return;

      // Cmd/Ctrl + D → 현재 선택된 블록 복제
      if (event.key.toLowerCase() === "d") {
        const { selectedBlockId: currentSelectedId } = (useEditorStore as any).getState();
        if (currentSelectedId) {
          event.preventDefault();
          duplicateBlock(currentSelectedId);
        }
        return;
      }

      // Cmd/Ctrl + Shift + Z → Redo
      if (event.key.toLowerCase() === "z" && event.shiftKey) {
        event.preventDefault();
        redo();
        return;
      }

      // Cmd/Ctrl + Z → Undo
      if (event.key.toLowerCase() === "z") {
        event.preventDefault();
        undo();
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [undo, redo, selectedBlockIds, replaceBlocks, removeBlock]);

  const renderBlocks = (targetBlocks: Block[]) =>
    targetBlocks.map((block) => {
      const isSelected =
        (selectedBlockId != null && block.id === selectedBlockId) || selectedBlockIds.includes(block.id);
      const isEditing = block.id === editingBlockId;

      return (
        <Fragment key={block.id}>
          <BlockDropzone id={`dropzone-before:${block.id}`} />
          <SortableEditorBlock
            block={block}
            isSelected={isSelected}
            selectedBlockId={selectedBlockId ?? null}
            selectedBlockIds={selectedBlockIds}
            setSelectedBlockIds={setSelectedBlockIds}
            isEditing={isEditing}
            editingText={editingText}
            startEditing={startEditing}
            commitEditing={commitEditing}
            cancelEditing={cancelEditing}
            setEditingText={setEditingText}
            selectBlock={selectBlock}
            selectedListItemId={selectedListItemId ?? null}
            selectListItem={selectListItem}
            allBlocks={blocks}
            renderBlocks={renderBlocks}
            updateBlock={updateBlock}
          />
        </Fragment>
      );
    });

  return (
    <main className="h-screen flex flex-col overflow-hidden">
      <header className="border-b border-slate-800 px-6 py-4 flex items-center justify-between relative z-20 bg-slate-950/80 backdrop-blur">
        <div className="flex items-center gap-3">
          <span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-slate-900 border border-sky-700 shadow-sm">
            <Pencil className="w-4 h-4 text-sky-400" aria-hidden="true" />
          </span>
          <div className="flex flex-col">
            <h1 className="text-xl font-semibold">Page Editor</h1>
            <p className="text-xs text-slate-400">빌더 MVP용 에디터 페이지 골격</p>
          </div>
        </div>
        <div className="flex items-center gap-2 text-xs">
          <Link
            href="/projects"
            className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800"
          >
            <ListChecks className="w-3 h-3" aria-hidden="true" />
            <span>프로젝트 목록</span>
          </Link>
          <Link
            href="/preview"
            className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800"
          >
            <Eye className="w-3 h-3" aria-hidden="true" />
            <span>프리뷰 열기</span>
          </Link>
          <button
            type="button"
            className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"
            onClick={undo}
            disabled={!canUndo}
          >
            <Undo2 className="w-3 h-3" aria-hidden="true" />
            <span>실행 취소</span>
          </button>
          <button
            type="button"
            className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed"
            onClick={redo}
            disabled={!canRedo}
          >
            <Redo2 className="w-3 h-3" aria-hidden="true" />
            <span>다시 실행</span>
          </button>
          <div className="relative">
            <button
              type="button"
              className="inline-flex items-center gap-1 rounded border border-slate-700 bg-slate-900 px-3 py-1 text-slate-100 hover:bg-slate-800"
              onClick={() => setMenuOpen((prev) => !prev)}
            >
              <FilePlus2 className="w-3 h-3" aria-hidden="true" />
              <span>메뉴</span>
              <span className="text-[9px]">▼</span>
            </button>
            {menuOpen && (
              <div className="absolute right-0 mt-1 w-48 rounded border border-slate-700 bg-slate-900 shadow-lg text-[11px] z-30">
                <button
                  type="button"
                  className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100"
                  onClick={() => {
                    setMenuOpen(false);
                    setActiveModal("project");
                  }}
                >
                  <span className="inline-flex items-center gap-2">
                    <FilePlus2 className="w-3 h-3" aria-hidden="true" />
                    <span>프로젝트 저장/불러오기</span>
                  </span>
                </button>
                <button
                  type="button"
                  className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100 border-t border-slate-800"
                  onClick={() => {
                    setMenuOpen(false);
                    setActiveModal("json");
                  }}
                >
                  <span className="inline-flex items-center gap-2">
                    <FilePlus2 className="w-3 h-3" aria-hidden="true" />
                    <span>JSON 내보내기/불러오기</span>
                  </span>
                </button>
                <button
                  type="button"
                  className="w-full px-3 py-2 text-left hover:bg-red-900/60 text-red-100 border-t border-slate-800"
                  onClick={() => {
                    setMenuOpen(false);
                    void handleDeleteProject();
                  }}
                >
                  <span className="inline-flex items-center gap-2">
                    <Trash2 className="w-3 h-3" aria-hidden="true" />
                    <span>프로젝트 삭제</span>
                  </span>
                </button>
                <button
                  type="button"
                  className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100 border-t border-slate-800"
                  onClick={() => {
                    setMenuOpen(false);
                    void handleExportZip();
                  }}
                >
                  <span className="inline-flex items-center gap-2">
                    <FilePlus2 className="w-3 h-3" aria-hidden="true" />
                    <span>페이지 파일로 내보내기 (ZIP)</span>
                  </span>
                </button>
                <button
                  type="button"
                  className="w-full px-3 py-2 text-left hover:bg-red-900/60 text-red-100 border-t border-slate-800"
                  onClick={() => {
                    setMenuOpen(false);
                    if (window.confirm("캔버스를 모두 초기화할까요? 이 작업은 실행 취소로만 되돌릴 수 있습니다.")) {
                      handleClearCanvas();
                    }
                  }}
                >
                  <span className="inline-flex items-center gap-2">
                    <Trash2 className="w-3 h-3" aria-hidden="true" />
                    <span>캔버스 초기화</span>
                  </span>
                </button>
              </div>
            )}
          </div>
        </div>
      </header>
      <section className="flex flex-1 overflow-hidden">
        <BlocksSidebar />
        <EditorCanvas
          blocks={blocks}
          rootBlocks={rootBlocks}
          selectedBlockId={selectedBlockId}
          editingBlockId={editingBlockId}
          editingText={editingText}
          activeDragId={activeDragId}
          sensors={sensors}
          onCanvasEmptyClick={() => selectBlock(null)}
          renderBlocks={renderBlocks}
          handleDragStart={handleDragStart}
          handleDragEnd={handleDragEnd}
          handleDragCancel={handleDragCancel}
          projectConfig={projectConfig}
        />
        <PropertiesSidebar
          blocks={blocks}
          selectedBlockId={selectedBlockId}
          selectedListItemId={selectedListItemId ?? null}
          updateBlock={(id, partial) => updateBlock(id, partial as any)}
          removeBlock={removeBlock}
          duplicateBlock={duplicateBlock}
          editingBlockId={editingBlockId}
          setEditingText={setEditingText}
          onMoveSelectedItemUp={(blockId) => moveSelectedListItemUp(blockId)}
          onMoveSelectedItemDown={(blockId) => moveSelectedListItemDown(blockId)}
          onIndentSelectedItem={(blockId) => indentSelectedListItem(blockId)}
          onOutdentSelectedItem={(blockId) => outdentSelectedListItem(blockId)}
        />
      </section>

      {activeModal === "project" && (
        <div className="fixed inset-0 z-30 flex items-center justify-center bg-black/60">
          <div className="w-full max-w-md rounded-lg border border-slate-700 bg-slate-900 p-4 text-xs text-slate-100 shadow-xl">
            <div className="flex items-center justify-between mb-3">
              <h3 className="text-sm font-medium">프로젝트 저장 / 불러오기</h3>
              <button
                type="button"
                className="text-slate-400 hover:text-slate-100 text-sm"
                onClick={() => setActiveModal(null)}
              >
                ✕
              </button>
            </div>
            <div className="space-y-2">
              <label className="flex flex-col gap-1">
                <span className="text-slate-400">프로젝트 제목</span>
                <input
                  className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
                  value={projectConfig.title ?? ""}
                  onChange={(e) => updateProjectConfig({ title: e.target.value })}
                />
              </label>
              <label className="flex flex-col gap-1">
                <span className="text-slate-400">프로젝트 주소 (예: my-landing)</span>
                <input
                  className="w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 text-xs outline-none focus:border-sky-500"
                  value={projectConfig.slug ?? ""}
                  onChange={(e) => updateProjectConfig({ slug: e.target.value })}
                />
              </label>
              <div className="flex gap-2">
                <button
                  type="button"
                  className="flex-1 rounded border border-sky-700 bg-sky-950 px-2 py-1 text-sky-100 hover:bg-sky-900"
                  onClick={handleSaveProject}
                >
                  저장 (로컬 + 서버)
                </button>
                <button
                  type="button"
                  className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-slate-100 hover:bg-slate-800"
                  onClick={handleLoadProject}
                >
                  불러오기
                </button>
              </div>
              {projectMessage && (
                <p className="text-[11px] text-slate-300 mt-1">{projectMessage}</p>
              )}
            </div>
          </div>
        </div>
      )}

      {/* Export 미리보기 기능은 제거되었습니다. */}

      {activeModal === "json" && (
        <div className="fixed inset-0 z-30 flex items-center justify-center bg-black/60">
          <div className="w-full max-w-2xl rounded-lg border border-slate-700 bg-slate-900 p-4 text-xs text-slate-100 shadow-xl">
            <div className="flex items-center justify-between mb-3">
              <h3 className="text-sm font-medium">JSON Export / Import</h3>
              <button
                type="button"
                className="text-slate-400 hover:text-slate-100 text-sm"
                onClick={() => setActiveModal(null)}
              >
                ✕
              </button>
            </div>
            <div className="space-y-3">
              <div className="flex gap-2 mb-1">
                <button
                  type="button"
                  className="flex-1 rounded border border-slate-700 bg-slate-950 px-2 py-1 hover:bg-slate-800"
                  onClick={handleExportJson}
                >
                  JSON 내보내기
                </button>
                <button
                  type="button"
                  className="rounded border border-red-700 bg-red-950 px-2 py-1 text-red-100 hover:bg-red-900"
                  onClick={handleClearCanvas}
                >
                  캔버스 초기화
                </button>
              </div>
              <div className="grid grid-cols-2 gap-3">
                <label className="flex flex-col gap-1">
                  <span className="text-slate-400">에디터 상태 JSON</span>
                  <textarea
                    className="w-full h-48 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[10px] font-mono outline-none focus:border-sky-500"
                    value={exportJson}
                    readOnly
                  />
                </label>
                <label className="flex flex-col gap-1">
                  <span className="text-slate-400">JSON에서 불러오기</span>
                  <textarea
                    className="w-full h-48 rounded border border-slate-700 bg-slate-950 px-2 py-1 text-[10px] font-mono outline-none focus:border-sky-500"
                    value={importJson}
                    onChange={(e) => setImportJson(e.target.value)}
                  />
                  <button
                    type="button"
                    className="mt-1 w-full rounded border border-slate-700 bg-slate-950 px-2 py-1 hover:bg-slate-800"
                    onClick={handleApplyImportJson}
                  >
                    JSON 적용하기
                  </button>
                </label>
              </div>
            </div>
          </div>
        </div>
      )}
    </main>
  );
}

interface DragPreviewProps {
  block: Block | null;
}

function DragPreview({ block }: DragPreviewProps) {
  if (!block) return null;

  // 블록 타입별로 간단한 고스트 카드 UI를 렌더링한다.
  return (
    <div className="pointer-events-none rounded border border-sky-500 bg-slate-900/80 px-3 py-2 text-xs text-slate-100 shadow-lg opacity-90 min-w-[160px] max-w-[260px]">
      <div className="text-[10px] uppercase tracking-wide text-sky-300 mb-1">
        {block.type === "text"
          ? "Text"
          : block.type === "button"
          ? "Button"
          : block.type === "image"
          ? "Image"
          : block.type === "divider"
          ? "Divider"
          : "Section"}
      </div>
      <div className="truncate">
        {block.type === "text"
          ? (block.props as TextBlockProps).text || "텍스트 블록"
          : block.type === "button"
          ? (block.props as ButtonBlockProps).label || "버튼 블록"
          : block.type === "image"
          ? (block.props as ImageBlockProps).alt || "이미지 블록"
          : block.type === "list"
          ? "리스트 블록"
          : block.type === "divider"
          ? "구분선 블록"
          : "섹션 블록"}
      </div>
    </div>
  );
}

interface TextInlineToolbarProps {
  align: TextBlockProps["align"];
  size: TextBlockProps["size"];
  onChangeAlign: (value: TextBlockProps["align"]) => void;
  onChangeSize: (value: TextBlockProps["size"]) => void;
}

interface SortableEditorBlockProps {
  block: Block;
  isSelected: boolean;
  selectedBlockId: string | null;
  selectedBlockIds: string[];
  setSelectedBlockIds: React.Dispatch<React.SetStateAction<string[]>>;
  isEditing: boolean;
  editingText: string;
  startEditing: (id: string, initialText: string) => void;
  commitEditing: () => void;
  cancelEditing: () => void;
  setEditingText: (value: string) => void;
  selectBlock: (id: string) => void;
  selectedListItemId: string | null;
  selectListItem: (id: string | null) => void;
  allBlocks: Block[];
  renderBlocks: (targetBlocks: Block[]) => ReactNode;
  updateBlock: (id: string, partial: Partial<TextBlockProps & ButtonBlockProps & FormBlockProps>) => void;
}

function SortableEditorBlock({
  block,
  isSelected,
  selectedBlockId,
  selectedBlockIds,
  setSelectedBlockIds,
  isEditing,
  editingText,
  startEditing,
  commitEditing,
  cancelEditing,
  setEditingText,
  selectBlock,
  selectedListItemId,
  selectListItem,
  allBlocks,
  renderBlocks,
  updateBlock,
}: SortableEditorBlockProps) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
    id: block.id,
  });

  const baseStyle: CSSProperties = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  let alignClass = "";
  let sizeClass = "";
  let leadingClass = "";
  let weightClass = "";
  let colorClass = "";
  let maxWidthClass = "";
  let decorationClass = "";
  const textStyleOverrides: CSSProperties = {};

  if (block.type === "text") {
    const textProps = block.props as TextBlockProps;
    const textTokens = computeTextEditorTokens(textProps);

    alignClass = textTokens.alignClass;
    sizeClass = textTokens.sizeClass;
    leadingClass = textTokens.leadingClass;
    weightClass = textTokens.weightClass;
    colorClass = textTokens.colorClass;
    maxWidthClass = textTokens.maxWidthClass;
    decorationClass = textTokens.decorationClass;
    Object.assign(textStyleOverrides, textTokens.styleOverrides);
  } else if (block.type === "button") {
    const buttonProps = block.props as ButtonBlockProps;
    const align = buttonProps.align ?? "left";
    alignClass =
      align === "center" ? "pb-text-center" : align === "right" ? "pb-text-right" : "pb-text-left";
  } else if (block.type === "image") {
    alignClass = "pb-text-left";
  } else if (block.type === "divider") {
    const dividerProps = block.props as DividerBlockProps;
    alignClass =
      dividerProps.align === "center"
        ? "pb-text-center"
        : dividerProps.align === "right"
        ? "pb-text-right"
        : "pb-text-left";
  } else if (block.type === "list") {
    const listProps = block.props as ListBlockProps;
    alignClass =
      listProps.align === "center"
        ? "pb-text-center"
        : listProps.align === "right"
        ? "pb-text-right"
        : "pb-text-left";
  } else if (block.type === "video") {
	  const videoProps = block.props as VideoBlockProps;
	  const align = videoProps.align ?? "center";
	  alignClass =
	    align === "center" ? "pb-text-center" : align === "right" ? "pb-text-right" : "pb-text-left";
	} else if (block.type === "section") {
    // 섹션 블록: 텍스트 크기 대신 섹션 자체에 패딩/배경을 입히므로 텍스트 클래스는 기본값만
    alignClass = "pb-text-left";
  } else if (block.type === "form") {
    // 폼 블록은 텍스트 클래스 대신 기본 정렬만 사용한다.
    alignClass = "pb-text-left";
  }

  return (
    <div
      ref={setNodeRef}
      style={{ ...baseStyle, ...textStyleOverrides }}
      data-testid="editor-block"
      data-block-id={block.id}
      data-selected={isSelected ? "true" : "false"}
      aria-selected={isSelected ? "true" : "false"}
      className={`rounded border px-3 py-2 cursor-text transition-colors ${alignClass} ${sizeClass} ${leadingClass} ${weightClass} ${colorClass} ${maxWidthClass} ${decorationClass} ${
        isSelected ? "border-sky-500 bg-slate-900/80" : "border-slate-700 bg-slate-900"
      }`}
      onClick={(event) => {
        // 멀티 선택(MVP): Cmd/Ctrl+클릭 시 선택 토글, 그 외에는 단일 선택으로 전환한다.
        const isMeta = event.metaKey || event.ctrlKey;
        if (isMeta) {
          event.stopPropagation();
          setSelectedBlockIds((prev) => {
            if (prev.includes(block.id)) {
              return prev.filter((id) => id !== block.id);
            }
            return [...prev, block.id];
          });
          return;
        }

        // 일반 클릭: 현재 블록만 단일 선택으로 유지한다.
        event.stopPropagation();
        setSelectedBlockIds([block.id]);
        selectBlock(block.id);
      }}
      onDoubleClick={(event) => {
        if (block.type === "text") {
          // 더블클릭 시 텍스트 블록 인라인 편집 모드로 진입한다.
          event.stopPropagation();
          startEditing(block.id, (block.props as TextBlockProps).text);
        }
      }}
    >
      <div className="flex items-start gap-2">
        <button
          type="button"
          className="mt-0.5 h-4 w-4 rounded border border-slate-700 bg-slate-900 text-[10px] flex items-center justify-center text-slate-400 hover:bg-slate-800"
          aria-label="블록 드래그 핸들"
          {...listeners}
          {...attributes}
        >
          ≡
        </button>
        <div className="flex-1">
          {block.type === "text" && (
            isEditing ? (
              <textarea
                className="w-full bg-transparent outline-none resize-none text-sm"
                aria-label="텍스트 블록 내용 편집"
                value={editingText}
                onChange={(e) => setEditingText(e.target.value)}
                onBlur={commitEditing}
                onKeyDown={(e) => {
                  if (e.key === "Enter" && !e.shiftKey) {
                    e.preventDefault();
                    commitEditing();
                  }
                  if (e.key === "Escape") {
                    e.preventDefault();
                    cancelEditing();
                  }
                }}
                rows={1}
                autoFocus
              />
            ) : (
              <div
                className={`whitespace-pre-wrap ${alignClass} ${sizeClass} ${leadingClass} ${weightClass} ${colorClass} ${maxWidthClass} ${decorationClass}`}
                style={textStyleOverrides}
              >
                {(block.props as TextBlockProps).text}
              </div>
            )
          )}
          {block.type === "formInput" && (() => {
            const inputProps = block.props as FormInputBlockProps;
            const inputType = inputProps.inputType ?? "text";

            // 필드 스타일: 텍스트 색상/배경/테두리/모서리 등은 커스텀 props 를 통해 제어한다.
            const inputTokens = computeFormInputEditorTokens(inputProps);
            const fieldStyle = inputTokens.fieldStyle;

            const labelLayout = inputProps.labelLayout ?? "stacked";
            const isInline = labelLayout === "inline";

            const inputAlignClass = inputTokens.inputAlignClass;

            // 에디터에서는 폼 입력 블록이 실제 입력 UI처럼 보이도록 라벨 + 인풋(또는 textarea)을 함께 렌더링한다.
            return (
              <div className={`text-xs ${isInline ? "flex items-center gap-2" : "flex flex-col gap-1"}`}>
                {inputProps.labelMode === "image" && inputProps.labelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={inputProps.labelImageUrl}
                    alt={inputProps.labelImageAlt || inputProps.label || "폼 라벨"}
                    className="h-4 w-auto"
                  />
                ) : (
                  <span className="text-slate-200 shrink-0">{inputProps.label}</span>
                )}
                {(() => {
                  // widthMode 와 labelLayout 에 따라 wrapper 의 Tailwind width/flex 클래스를 결정한다.
                  const widthClass = inputTokens.widthClass;

                  return (
                <div
                  data-testid="form-input-field"
                  style={fieldStyle}
                  className={`${widthClass} rounded border border-slate-700 bg-slate-950`}
                >
                  {inputType === "textarea" ? (
                    <textarea
                      className={`w-full min-h-[60px] bg-transparent px-2 py-1 text-xs outline-none ${inputAlignClass}`}
                      placeholder={inputProps.placeholder || inputProps.label}
                      aria-label={inputProps.label}
                      readOnly
                    />
                  ) : (
                    <input
                      className={`w-full bg-transparent px-2 py-1 text-xs outline-none ${inputAlignClass}`}
                      type={inputType === "email" ? "email" : "text"}
                      placeholder={inputProps.placeholder || inputProps.label}
                      aria-label={inputProps.label}
                      readOnly
                    />
                  )}
                </div>
                  );
                })()}
              </div>
            );
          })()}
          {block.type === "formSelect" && (() => {
            const selectProps = block.props as FormSelectBlockProps;
            const options = Array.isArray(selectProps.options) && selectProps.options.length > 0
              ? selectProps.options
              : [
                  { label: "옵션 1", value: "option_1" },
                  { label: "옵션 2", value: "option_2" },
                ];

            const selectTokens = computeFormSelectEditorTokens(selectProps);
            const fieldStyle = selectTokens.fieldStyle;

            // 에디터에서는 폼 셀렉트 블록이 라벨 + 셀렉트 박스로 보이도록 렌더링한다.
            return (
              <div className="flex flex-col gap-1 text-xs">
                {selectProps.labelMode === "image" && selectProps.labelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={selectProps.labelImageUrl}
                    alt={selectProps.labelImageAlt || selectProps.label || "폼 라벨"}
                    className="h-4 w-auto"
                  />
                ) : (
                  <span className="text-slate-200">{selectProps.label}</span>
                )}
                <div
                  style={fieldStyle}
                  className="w-full rounded border border-slate-700 bg-slate-950"
                >
                  <select
                    className="w-full bg-transparent px-2 py-1 text-xs outline-none"
                    aria-label={selectProps.label}
                    defaultValue={options[0]?.value}
                  >
                    {options.map((opt) => (
                      <option key={opt.value} value={opt.value}>
                        {opt.label}
                      </option>
                    ))}
                  </select>
                </div>
              </div>
            );
          })()}
          {block.type === "formRadio" && (() => {
            const radioProps = block.props as FormRadioBlockProps;
            const options = Array.isArray(radioProps.options) && radioProps.options.length > 0
              ? radioProps.options
              : [
                  { label: "옵션 A", value: "option_a" },
                  { label: "옵션 B", value: "option_b" },
                ];

            const groupLabelMode = radioProps.groupLabelMode ?? "text";

            const radioTokens = computeFormOptionGroupEditorTokens(radioProps);
            const fieldStyle = radioTokens.fieldStyle;

            return (
              <div className="flex flex-col gap-1 text-xs">
                {/* 그룹 타이틀은 텍스트/이미지 모드를 지원한다. */}
                {groupLabelMode === "image" && radioProps.groupLabelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={radioProps.groupLabelImageUrl}
                    alt={radioProps.groupLabel || "폼 그룹 타이틀"}
                    className="inline-block max-w-full h-auto"
                  />
                ) : (
                  <span className="text-slate-200">{radioProps.groupLabel}</span>
                )}
                <div
                  style={fieldStyle}
                  className="flex flex-col gap-1 rounded border border-slate-700 bg-slate-950"
                >
                  {options.map((opt) => (
                    <label key={opt.value} className="inline-flex items-center gap-2 text-slate-200">
                      <input
                        type="radio"
                        className="h-3 w-3 rounded border border-slate-700 bg-slate-950"
                        name={radioProps.formFieldName}
                        value={opt.value}
                        readOnly
                      />
                      {/* 옵션 옆 라벨은 옵션에 개별 이미지 URL 이 있는 경우 이미지로 렌더링한다. */}
                      {opt.labelImageUrl ? (
                        // eslint-disable-next-line @next/next/no-img-element
                        <img
                          src={opt.labelImageUrl}
                          alt={opt.label || radioProps.groupLabel || "폼 라벨"}
                          className="h-4 w-auto"
                        />
                      ) : (
                        <span>{opt.label}</span>
                      )}
                    </label>
                  ))}
                </div>
              </div>
            );
          })()}
          {block.type === "formCheckbox" && (() => {
            const checkboxProps = block.props as FormCheckboxBlockProps;
            const options = Array.isArray(checkboxProps.options) && checkboxProps.options.length > 0
              ? checkboxProps.options
              : [
                  { label: "체크 1", value: "check_1" },
                  { label: "체크 2", value: "check_2" },
                ];

            const groupLabelMode = checkboxProps.groupLabelMode ?? "text";

            const checkboxTokens = computeFormOptionGroupEditorTokens(checkboxProps);
            const fieldStyle = checkboxTokens.fieldStyle;

            return (
              <div className="flex flex-col gap-1 text-xs text-slate-200" style={fieldStyle}>
                {/* 체크박스 그룹 타이틀도 텍스트/이미지 모드를 지원한다. */}
                {groupLabelMode === "image" && checkboxProps.groupLabelImageUrl ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={checkboxProps.groupLabelImageUrl}
                    alt={checkboxProps.groupLabel || "폼 그룹 타이틀"}
                    className="inline-block max-w-full h-auto"
                  />
                ) : (
                  <span className="text-slate-200">{checkboxProps.groupLabel}</span>
                )}
                <div className="flex flex-col gap-1">
                  {options.map((opt) => (
                    <label key={opt.value} className="inline-flex items-center gap-2">
                      <input
                        type="checkbox"
                        className="h-3 w-3 rounded border border-slate-700 bg-slate-950"
                        name={checkboxProps.formFieldName}
                        value={opt.value}
                        readOnly
                      />
                      {opt.labelImageUrl ? (
                        // eslint-disable-next-line @next/next/no-img-element
                        <img
                          src={opt.labelImageUrl}
                          alt={opt.label || checkboxProps.groupLabel || "폼 라벨"}
                          className="h-4 w-auto"
                        />
                      ) : (
                        <span>{opt.label}</span>
                      )}
                    </label>
                  ))}
                </div>
              </div>
            );
          })()}
          {block.type === "button" && (() => {
            const buttonProps = block.props as ButtonBlockProps;

            const pbTokens = computeButtonPbTokens({
              align: buttonProps.align ?? "left",
              size: buttonProps.size ?? "md",
              variant: buttonProps.variant ?? "solid",
              colorPalette: buttonProps.colorPalette ?? "primary",
              borderRadius: buttonProps.borderRadius ?? "md",
              fullWidth: buttonProps.fullWidth ?? false,
              widthMode: buttonProps.widthMode ?? undefined,
              widthPx: typeof buttonProps.widthPx === "number" ? buttonProps.widthPx : undefined,
              paddingX: typeof buttonProps.paddingX === "number" ? buttonProps.paddingX : undefined,
              paddingY: typeof buttonProps.paddingY === "number" ? buttonProps.paddingY : undefined,
              fillColorCustom: buttonProps.fillColorCustom ?? undefined,
              strokeColorCustom: buttonProps.strokeColorCustom ?? undefined,
              textColorCustom: buttonProps.textColorCustom ?? undefined,
            });

            const editorTokens = computeButtonEditorTokens(buttonProps);

            return (
              <div className={editorTokens.wrapperWidthClass}>
                <button
                  type="button"
                  className={`pb-btn-base ${pbTokens.sizeClass} ${pbTokens.radiusClass} ${pbTokens.variantClass} w-full`}
                  style={editorTokens.buttonStyle}
                  aria-label={buttonProps.label}
                  onClick={(e) => {
                    // 에디터 내 버튼 클릭은 선택만 유지하고 실제 이동은 막는다.
                    e.stopPropagation();
                  }}
                >
                  <span className="whitespace-pre-wrap">{buttonProps.label}</span>
                </button>
              </div>
            );
          })()}
          {block.type === "video" && (() => {
            const videoProps = block.props as VideoBlockProps;
            const normalizedUrl = normalizeVideoSourceUrl(videoProps.sourceUrl ?? "");
            const platform = resolveVideoPlatform(normalizedUrl, videoProps.platform ?? "auto");
            const embedUrl = buildVideoEmbedUrl(normalizedUrl, platform, { enableVimeoEmbed: false });
            const tokens = computeVideoEditorTokens(videoProps);

            const startTimeSec =
              typeof videoProps.startTimeSec === "number" && videoProps.startTimeSec >= 0
                ? videoProps.startTimeSec
                : null;
            const endTimeSec =
              typeof videoProps.endTimeSec === "number" && videoProps.endTimeSec >= 0
                ? videoProps.endTimeSec
                : null;
            const isEmbed = platform === "youtube" || platform === "vimeo";

            return (
              <div className={`w-full flex ${tokens.justifyClass}`}>
                <div>
                  <div className={`pb-video-wrapper${tokens.aspectClass}`} style={tokens.wrapperStyle}>
                    {isEmbed ? (
                      <iframe
                        title={videoProps.titleText && videoProps.titleText.trim() !== "" ? videoProps.titleText.trim() : "비디오"}
                        src={embedUrl}
                        className="pb-video-frame"
                        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
                        allowFullScreen
                      />
                    ) : (
                      <video
                        data-testid="editor-video"
                        className="pb-video-frame"
                        src={normalizedUrl !== "" ? normalizedUrl : undefined}
                        style={tokens.videoStyle}
                        poster={videoProps.posterImageSrc && videoProps.posterImageSrc.trim() !== "" ? videoProps.posterImageSrc.trim() : undefined}
                        aria-label={videoProps.ariaLabel && videoProps.ariaLabel.trim() !== "" ? videoProps.ariaLabel.trim() : undefined}
                        onLoadedMetadata={(event) => {
                          if (startTimeSec != null) {
                            const el = event.currentTarget;
                            try {
                              el.currentTime = startTimeSec;
                            } catch {
                              // ignore
                            }
                          }
                        }}
                        onTimeUpdate={(event) => {
                          if (endTimeSec != null) {
                            const el = event.currentTarget;
                            if (el.currentTime >= endTimeSec) {
                              el.pause();
                              try {
                                if (!Number.isNaN(endTimeSec)) {
                                  el.currentTime = endTimeSec;
                                }
                              } catch {
                                // ignore
                              }
                            }
                          }
                        }}
                        controls={videoProps.controls !== false}
                        autoPlay={!!videoProps.autoplay}
                        loop={!!videoProps.loop}
                        muted={!!videoProps.muted}
                      />
                    )}
                  </div>
                  {videoProps.captionText && videoProps.captionText.trim() !== "" && (
                    <p
                      data-testid="editor-video-caption"
                      className="mt-2 text-xs text-slate-400 hidden"
                    >
                      {videoProps.captionText}
                    </p>
                  )}
                </div>
              </div>
            );
          })()}
          {block.type === "image" && (() => {
            const imageProps = block.props as ImageBlockProps;
            const hasSrc = imageProps.src.trim().length > 0;
            const align = imageProps.align ?? "center";
            const alignClass =
              align === "left" ? "justify-start" : align === "right" ? "justify-end" : "justify-center";
            const tokens = computeImageEditorTokens({
              widthMode: imageProps.widthMode,
              widthPx: imageProps.widthPx,
              borderRadius: imageProps.borderRadius,
              borderRadiusPx: imageProps.borderRadiusPx,
            });

            const containerStyle: React.CSSProperties = {};
            if (typeof tokens.widthPx === "number") {
              containerStyle.width = `${tokens.widthPx}px`;
            }

            const imageStyle: React.CSSProperties = {
              maxWidth: "100%",
              height: "auto",
              borderRadius: `${tokens.radiusPx}px`,
            };

            return (
              <div
                className={`w-full border border-dashed border-slate-700 bg-slate-900/40 rounded flex items-center overflow-hidden ${alignClass}`}
              >
                {hasSrc ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    src={imageProps.src}
                    alt={imageProps.alt}
                    className="object-contain"
                    style={{ ...containerStyle, ...imageStyle }}
                  />
                ) : (
                  <span className="text-[10px] text-slate-500 px-2 py-4">
                    이미지 URL 을 입력하거나 파일을 업로드하면 여기에서 미리보기가 표시됩니다.
                  </span>
                )}
              </div>
            );
          })()}
          {block.type === "divider" && (() => {
            const dividerProps = block.props as DividerBlockProps;
            const tokens = computeDividerEditorTokens(dividerProps);

            // 색상: colorHex 가 있으면 사용, 없으면 기본 슬레이트 색상
            const borderColor = tokens.borderColor;

            // 길이/너비: widthMode/widthPx 에 따라 가로 길이를 조정
            const widthPx = tokens.widthPx;

            // 위/아래 여백: marginYPx 가 있으면 px 값 그대로 사용, 없으면 토큰 기반 기본값
            const margin = tokens.marginPx;

            const innerStyle: React.CSSProperties = {
              borderColor,
            };
            if (typeof widthPx === "number") {
              innerStyle.width = `${widthPx}px`;
            }

            return (
              <div className="w-full" style={{ marginTop: margin, marginBottom: margin }}>
                <div
                  className={`${tokens.thicknessClass} ${tokens.innerWidthClass} border-t`}
                  style={innerStyle}
                />
              </div>
            );
          })()}
          {block.type === "list" && (() => {
            const listProps = block.props as ListBlockProps;
            const alignClass =
              listProps.align === "center"
                ? "text-center"
                : listProps.align === "right"
                ? "text-right"
                : "text-left";

            const tokens = computeListEditorTokens(listProps);

            // 줄 간 간격: gapYPx 숫자 값을 우선 사용하고, 없으면 gapY 토큰을 space-y 클래스로 매핑
            const gapPx = tokens.gapPx;

            // 타이포/색상 스타일
            const listStyle = tokens.listStyle;

            // 불릿 스타일 (none 인 경우만 제거, decimal 은 숫자형으로 매핑)
            const bulletStyle = tokens.bulletStyle;

            const itemsTree: ListItemNode[] =
              (listProps.itemsTree as ListItemNode[] | undefined) && listProps.itemsTree!.length > 0
                ? (listProps.itemsTree as ListItemNode[])
                : listProps.items && listProps.items.length > 0
                ? listProps.items.map((text, index) => ({
                    id: `${block.id}_item_${index + 1}`,
                    text,
                    children: [],
                  }))
                : [
                    {
                      id: `${block.id}_item_1`,
                      text: "리스트 아이템 1",
                      children: [],
                    },
                  ];

            const renderListNodes = (nodes: ListItemNode[], level: number): React.ReactNode => {
              const isOrderedLevel = listProps.ordered;
              const ListTag = (isOrderedLevel ? "ol" : "ul") as "ol" | "ul";

              return (
                <ListTag
                  className={`text-xs ${alignClass}`}
                  style={{
                    ...listStyle,
                    listStyleType:
                      bulletStyle === "none"
                        ? "none"
                        : bulletStyle,
                    paddingLeft: 12 + level * 8,
                  }}
                >
                  {nodes.map((node, index) => (
                    <li
                      key={node.id}
                      data-list-item-id={node.id}
                      className={
                        selectedListItemId === node.id
                          ? "rounded bg-sky-900/40 text-sky-100"
                          : undefined
                      }
                      style={index < nodes.length - 1 ? { marginBottom: gapPx } : undefined}
                    >
                      <span className="break-words align-middle">{node.text}</span>
                      <span className="ml-2 inline-flex items-center gap-1 text-[10px] text-slate-300 align-middle">
                        <button
                          type="button"
                          className="rounded border border-slate-700 px-1 py-0.5 hover:bg-slate-800"
                          onClick={(event) => {
                            event.stopPropagation();
                            selectBlock(block.id);
                            selectListItem(node.id);
                            (useEditorStore.getState() as any).moveSelectedListItemUp(block.id);
                          }}
                        >
                          위
                        </button>
                        <button
                          type="button"
                          className="rounded border border-slate-700 px-1 py-0.5 hover:bg-slate-800"
                          onClick={(event) => {
                            event.stopPropagation();
                            selectBlock(block.id);
                            selectListItem(node.id);
                            (useEditorStore.getState() as any).moveSelectedListItemDown(block.id);
                          }}
                        >
                          아래
                        </button>
                        <button
                          type="button"
                          className="rounded border border-slate-700 px-1 py-0.5 hover:bg-slate-800"
                          onClick={(event) => {
                            event.stopPropagation();
                            selectBlock(block.id);
                            selectListItem(node.id);
                            (useEditorStore.getState() as any).indentSelectedListItem(block.id);
                          }}
                        >
                          들여쓰기
                        </button>
                        <button
                          type="button"
                          className="rounded border border-slate-700 px-1 py-0.5 hover:bg-slate-800"
                          onClick={(event) => {
                            event.stopPropagation();
                            selectBlock(block.id);
                            selectListItem(node.id);
                            (useEditorStore.getState() as any).outdentSelectedListItem(block.id);
                          }}
                        >
                          내어쓰기
                        </button>
                      </span>
                      {node.children && node.children.length > 0 && renderListNodes(node.children, level + 1)}
                    </li>
                  ))}
                </ListTag>
              );
            };

            return <div className="w-full py-1">{renderListNodes(itemsTree, 0)}</div>;
          })()}
          {block.type === "section" && (() => {
            const sectionProps = block.props as SectionBlockProps;
            const isSectionSelected =
              isSelected ||
              (selectedBlockId != null &&
                allBlocks.some(
                  (candidate) => candidate.id === selectedBlockId && candidate.sectionId === block.id,
                ));
            const sectionTokens = computeSectionEditorTokens(sectionProps);

            const columns = sectionProps.columns && sectionProps.columns.length > 0
              ? sectionProps.columns
              : [{ id: `${block.id}_col_fallback`, span: 12 }];

            return (
              <div
                data-testid="editor-section"
                className={`w-full ${sectionTokens.bgClass} ${sectionTokens.pyClass} rounded border ${
                  isSectionSelected ? "border-sky-500" : "border-dashed border-slate-700"
                }`}
                style={sectionTokens.wrapperStyle}
              >
                {sectionTokens.hasBackgroundVideo && (
                  <video
                    data-testid="editor-section-bg-video"
                    className="absolute inset-0 w-full h-full object-cover"
                    src={sectionTokens.backgroundVideoSrc!}
                    autoPlay
                    loop
                    muted
                    playsInline
                  />
                )}
                <div
                  className="mx-auto px-4"
                  style={sectionTokens.innerWrapperStyle}
                >
                  <div
                    className={`flex ${sectionTokens.alignItemsClass}`}
                    style={sectionTokens.columnsContainerStyle}
                  >
                    {columns.map((col) => {
                      const basis = `${(col.span / 12) * 100}%`;
                      const columnBlocks = allBlocks.filter(
                        (b) => b.sectionId === block.id && b.columnId === col.id,
                      );
                      const droppableId = `column:${block.id}:${col.id}`;
                      return (
                        <ColumnDroppable
                          key={col.id}
                          id={droppableId}
                          sectionId={block.id}
                          columnId={col.id}
                          basis={basis}
                          blocks={columnBlocks}
                          renderBlocks={renderBlocks}
                          span={col.span}
                        />
                      );
                    })}
                  </div>
                </div>
              </div>
            );
          })()}
        </div>
      </div>
    </div>
  );
}

interface BlockDropzoneProps {
  id: string;
  testId?: string;
}

function BlockDropzone({ id, testId }: BlockDropzoneProps) {
  const { setNodeRef, isOver } = useDroppable({ id });

  return (
    <div
      ref={setNodeRef}
      data-testid={testId}
      className={`h-2 w-full rounded-full transition-colors ${
        isOver ? "bg-sky-500" : "bg-sky-500/40"
      }`}
    />
  );
}

interface ColumnDroppableProps {
  id: string;
  sectionId: string;
  columnId: string;
  basis: string;
  blocks: Block[];
  renderBlocks: (targetBlocks: Block[]) => ReactNode;
  span: number;
}

function ColumnDroppable({ id, sectionId, columnId, basis, blocks, renderBlocks, span }: ColumnDroppableProps) {
  const { setNodeRef } = useDroppable({ id });
  const { over } = useDndContext();

  // 현재 드롭 대상이 이 컬럼이거나, 이 컬럼 안 블록들의 dropzone-before / dropzone-after 인 경우
  // 컬럼 박스를 강조해서 사용자가 어느 컬럼으로 이동 중인지 명확히 보여준다.
  const overId = over?.id ? String(over.id) : null;

  const isActiveColumn = (() => {
    if (!overId) return false;

    // 0) 섹션 블록 자체 위에 있는 경우: 해당 섹션의 컬럼 전체를 하이라이트한다.
    if (overId === sectionId) return true;

    // 1) 이 컬럼 droppable 자체 위에 있는 경우
    if (overId === id) return true;

    // 2) 이 컬럼 안 블록들 앞 드롭존 또는 섹션 바로 위 드롭존: dropzone-before:*
    if (overId.startsWith("dropzone-before:")) {
      const [, targetId] = overId.split(":");
      // 섹션 바로 위 루트 드롭존(dropzone-before:sectionId)도 이 섹션의 컬럼을 활성화한다.
      if (targetId === sectionId) {
        return true;
      }

      if (targetId && blocks.some((b) => b.id === targetId)) {
        return true;
      }
    }

    // 3) 이 컬럼 맨 아래 드롭존: dropzone-after:sectionId:columnId
    if (overId === `dropzone-after:${sectionId}:${columnId}`) {
      return true;
    }

    return false;
  })();

  return (
    <div
      ref={setNodeRef}
      data-section-id={sectionId}
      data-column-id={columnId}
      style={{ flexBasis: basis }}
    >
      <div
        className={`border rounded flex flex-col gap-2 items-stretch justify-start px-2 py-2 bg-slate-950/40 ${
          isActiveColumn ? "border-sky-500" : "border-slate-800/80"
        }`}
        style={{}}
      >
        {blocks.length === 0 ? (
          <span className="text-[11px] px-2 text-center text-slate-500">
            {`컬럼 영역 (span ${span}/12)`}
          </span>
        ) : (
          <SortableContext items={blocks.map((b) => b.id)} strategy={verticalListSortingStrategy}>
            <div className="flex flex-col gap-2">
              {renderBlocks(blocks)}
              {/* 컬럼 맨 아래 드롭존: 블록을 이 컬럼의 최하단으로 이동 */}
              <BlockDropzone id={`dropzone-after:${sectionId}:${columnId}`} />
            </div>
          </SortableContext>
        )}
      </div>
    </div>
  );
}
