import {
  CloseOutlined,
  CodeOutlined,
  CopyOutlined,
  DesktopOutlined,
  MinusOutlined,
  MobileOutlined,
  ReloadOutlined,
} from '@ant-design/icons';
import { Button } from 'antd';
import {
  useEffect,
  useMemo,
  useRef,
  useState,
  type MouseEvent as ReactMouseEvent,
  type PointerEvent as ReactPointerEvent,
} from 'react';
import { createPortal } from 'react-dom';
import { PreviewAppWindow } from './PreviewAppWindow';
import { copyTextToClipboard } from '../../utils/clipboard';
import {
  resolvePreviewAppOrigin,
  type PreviewRuntimeConsoleBridgeMessage,
  type PreviewRuntimeConsoleLevel,
  type PreviewTargetDescriptor,
} from './previewRuntime';

type PreviewAppOverlayProps = {
  pathname: string;
  search?: string;
  targetDescriptor?: PreviewTargetDescriptor;
  onClose: () => void;
};

type DragPosition = {
  x: number;
  y: number;
};

type DetachedConsoleSize = {
  width: number;
  height: number;
};

const HEADER_HEIGHT = 44;
const MINIMIZED_WIDTH = 168;
const MOBILE_SHELL_WIDTH = 430;
const MOBILE_SHELL_HEIGHT = 860;
const VIEWPORT_PADDING = 12;
const MAX_CONSOLE_ENTRIES = 300;
const DETACHED_CONSOLE_WIDTH = 460;
const DETACHED_CONSOLE_HEIGHT = 340;
const DETACHED_CONSOLE_MIN_WIDTH = 360;
const DETACHED_CONSOLE_MAX_WIDTH = 840;
const DETACHED_CONSOLE_MIN_HEIGHT = 240;
const DETACHED_CONSOLE_MAX_HEIGHT = 620;
const DETACHED_CONSOLE_DRAGGING_CLASS = 'preview-app-overlay-console-dragging';
const CONSOLE_LEVELS: PreviewRuntimeConsoleLevel[] = ['log', 'info', 'warn', 'error', 'debug'];

type PreviewConsoleEntry = PreviewRuntimeConsoleBridgeMessage & {
  id: string;
};

function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}

function formatConsoleEntryCopyText(entry: PreviewConsoleEntry) {
  return [
    `${entry.level.toUpperCase()} ${new Date(entry.timestamp).toLocaleTimeString('ko-KR', {
      hour12: false,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
    })}`,
    entry.args.join('\n'),
  ]
    .filter((value) => value.trim().length > 0)
    .join('\n');
}

function getDefaultMinimizedPosition(): DragPosition {
  if (typeof window === 'undefined') {
    return {
      x: VIEWPORT_PADDING,
      y: VIEWPORT_PADDING,
    };
  }

  return {
    x: Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING),
    y: Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING),
  };
}

function getMobileShellMetrics() {
  if (typeof window === 'undefined') {
    return {
      width: MOBILE_SHELL_WIDTH,
      height: MOBILE_SHELL_HEIGHT,
    };
  }

  return {
    width: Math.min(MOBILE_SHELL_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)),
    height: Math.min(MOBILE_SHELL_HEIGHT, Math.max(520, window.innerHeight - VIEWPORT_PADDING * 2)),
  };
}

function getDefaultMobileShellPosition(): DragPosition {
  if (typeof window === 'undefined') {
    return {
      x: VIEWPORT_PADDING,
      y: VIEWPORT_PADDING,
    };
  }

  const { width, height } = getMobileShellMetrics();

  return {
    x: Math.max(VIEWPORT_PADDING, (window.innerWidth - width) / 2),
    y: Math.max(VIEWPORT_PADDING, (window.innerHeight - height) / 2),
  };
}

function getDetachedConsoleMetrics() {
  if (typeof window === 'undefined') {
    return {
      width: DETACHED_CONSOLE_WIDTH,
      height: DETACHED_CONSOLE_HEIGHT,
    };
  }

  return {
    width: Math.min(DETACHED_CONSOLE_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)),
    height: Math.min(DETACHED_CONSOLE_HEIGHT, Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2)),
  };
}

function getDetachedConsoleSizeBounds() {
  if (typeof window === 'undefined') {
    return {
      minWidth: DETACHED_CONSOLE_MIN_WIDTH,
      maxWidth: DETACHED_CONSOLE_MAX_WIDTH,
      minHeight: DETACHED_CONSOLE_MIN_HEIGHT,
      maxHeight: DETACHED_CONSOLE_MAX_HEIGHT,
    };
  }

  return {
    minWidth: Math.min(DETACHED_CONSOLE_MIN_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)),
    maxWidth: Math.max(
      Math.min(DETACHED_CONSOLE_MAX_WIDTH, window.innerWidth - VIEWPORT_PADDING * 2),
      Math.min(DETACHED_CONSOLE_MIN_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)),
    ),
    minHeight: Math.min(
      DETACHED_CONSOLE_MIN_HEIGHT,
      Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2),
    ),
    maxHeight: Math.max(
      Math.min(DETACHED_CONSOLE_MAX_HEIGHT, window.innerHeight - VIEWPORT_PADDING * 2),
      Math.min(DETACHED_CONSOLE_MIN_HEIGHT, Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2)),
    ),
  };
}

function normalizeDetachedConsoleSize(size: DetachedConsoleSize): DetachedConsoleSize {
  const { minWidth, maxWidth, minHeight, maxHeight } = getDetachedConsoleSizeBounds();

  return {
    width: clamp(size.width, minWidth, maxWidth),
    height: clamp(size.height, minHeight, maxHeight),
  };
}

function getDefaultDetachedConsoleSize(): DetachedConsoleSize {
  return normalizeDetachedConsoleSize({
    width: DETACHED_CONSOLE_WIDTH,
    height: DETACHED_CONSOLE_HEIGHT,
  });
}

function getDefaultDetachedConsolePosition(): DragPosition {
  if (typeof window === 'undefined') {
    return {
      x: VIEWPORT_PADDING,
      y: VIEWPORT_PADDING,
    };
  }

  const { width, height } = getDetachedConsoleMetrics();

  return {
    x: Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING),
    y: Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING),
  };
}

export function PreviewAppOverlay({
  pathname,
  search = '',
  targetDescriptor = null,
  onClose,
}: PreviewAppOverlayProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const minimizedPositionRef = useRef<DragPosition>(getDefaultMinimizedPosition());
  const mobileShellPositionRef = useRef<DragPosition>(getDefaultMobileShellPosition());
  const detachedConsolePositionRef = useRef<DragPosition>(getDefaultDetachedConsolePosition());
  const detachedConsoleSizeRef = useRef<DetachedConsoleSize>(getDefaultDetachedConsoleSize());
  const rootRef = useRef<HTMLDivElement>(null);
  const consoleBodyRef = useRef<HTMLDivElement>(null);
  const dragStateRef = useRef<{
    pointerId: number;
    lastX: number;
    lastY: number;
    captureTarget: HTMLDivElement;
  } | null>(null);
  const detachedConsoleDragStateRef = useRef<{
    pointerId: number;
    lastX: number;
    lastY: number;
    captureTarget: HTMLDivElement;
  } | null>(null);
  const detachedConsoleResizeStateRef = useRef<{
    pointerId: number;
    startX: number;
    startY: number;
    startWidth: number;
    startHeight: number;
    captureTarget: HTMLDivElement;
  } | null>(null);
  const dragMovedRef = useRef(false);
  const [minimized, setMinimized] = useState(false);
  const [deviceMode, setDeviceMode] = useState<'desktop' | 'mobile'>('mobile');
  const [consoleOpen, setConsoleOpen] = useState(false);
  const [consoleDetached, setConsoleDetached] = useState(false);
  const [consoleEntries, setConsoleEntries] = useState<PreviewConsoleEntry[]>([]);
  const [consoleLevelFilter, setConsoleLevelFilter] = useState<PreviewRuntimeConsoleLevel[]>(CONSOLE_LEVELS);
  const [reloadKey, setReloadKey] = useState(0);
  const [isMobileViewport, setIsMobileViewport] = useState(() =>
    typeof window !== 'undefined' ? window.matchMedia('(max-width: 768px)').matches : false,
  );
  const [position, setPosition] = useState<DragPosition>(() => minimizedPositionRef.current);
  const [detachedConsolePosition, setDetachedConsolePosition] = useState<DragPosition>(
    () => detachedConsolePositionRef.current,
  );
  const [detachedConsoleSize, setDetachedConsoleSize] = useState<DetachedConsoleSize>(
    () => detachedConsoleSizeRef.current,
  );
  const isDesktopMobileShell = !minimized && deviceMode === 'mobile' && !isMobileViewport;
  const canDetachConsole = !isMobileViewport;
  const showDetachedConsole = consoleOpen && consoleDetached && canDetachConsole;
  const showAttachedConsole = consoleOpen && !showDetachedConsole;
  const previewOrigin = useMemo(() => resolvePreviewAppOrigin(), []);

  const clearDetachedConsoleDraggingState = () => {
    if (typeof document === 'undefined') {
      return;
    }

    document.body.classList.remove(DETACHED_CONSOLE_DRAGGING_CLASS);
  };

  useEffect(() => {
    setConsoleEntries([]);
  }, [pathname, search, targetDescriptor, deviceMode]);

  useEffect(() => {
    document.body.classList.add('preview-app-overlay-open');

    return () => {
      document.body.classList.remove('preview-app-overlay-open');
      document.body.classList.remove(DETACHED_CONSOLE_DRAGGING_CLASS);
    };
  }, []);

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

    const mediaQuery = window.matchMedia('(max-width: 768px)');
    const handleChange = (event: MediaQueryListEvent) => {
      setIsMobileViewport(event.matches);
    };

    setIsMobileViewport(mediaQuery.matches);
    mediaQuery.addEventListener('change', handleChange);

    return () => {
      mediaQuery.removeEventListener('change', handleChange);
    };
  }, []);

  useEffect(() => {
    if (isMobileViewport) {
      setConsoleDetached(false);
    }
  }, [isMobileViewport]);

  useEffect(() => {
    if (!minimized && !isDesktopMobileShell) {
      return;
    }

    const resizeListener = () => {
      if (minimized) {
        setPosition((current) => {
          const nextPosition = {
            x: clamp(
              current.x,
              VIEWPORT_PADDING,
              Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING),
            ),
            y: clamp(
              current.y,
              VIEWPORT_PADDING,
              Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING),
            ),
          };

          minimizedPositionRef.current = nextPosition;

          return nextPosition;
        });
        return;
      }

      const { width, height } = getMobileShellMetrics();

      setPosition((current) => ({
        x: clamp(
          current.x,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING),
        ),
        y: clamp(
          current.y,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING),
        ),
      }));
    };

    window.addEventListener('resize', resizeListener);
    resizeListener();

    return () => {
      window.removeEventListener('resize', resizeListener);
    };
  }, [isDesktopMobileShell, minimized]);

  useEffect(() => {
    if (minimized) {
      minimizedPositionRef.current = position;
    }
  }, [minimized, position]);

  useEffect(() => {
    if (isDesktopMobileShell) {
      mobileShellPositionRef.current = position;
    }
  }, [isDesktopMobileShell, position]);

  useEffect(() => {
    if (!minimized && !isDesktopMobileShell) {
      dragStateRef.current = null;
      dragMovedRef.current = false;
      return;
    }

    const handlePointerMove = (event: PointerEvent) => {
      const dragState = dragStateRef.current;

      if (!dragState || dragState.pointerId !== event.pointerId) {
        return;
      }

      const deltaX = event.clientX - dragState.lastX;
      const deltaY = event.clientY - dragState.lastY;

      if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {
        dragMovedRef.current = true;
      }

      dragState.lastX = event.clientX;
      dragState.lastY = event.clientY;

      const maxX = minimized
        ? Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING)
        : Math.max(
            VIEWPORT_PADDING,
            window.innerWidth - getMobileShellMetrics().width - VIEWPORT_PADDING,
          );
      const maxY = minimized
        ? Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING)
        : Math.max(
            VIEWPORT_PADDING,
            window.innerHeight - getMobileShellMetrics().height - VIEWPORT_PADDING,
          );

      setPosition((current) => {
        const nextPosition = {
          x: clamp(current.x + deltaX, VIEWPORT_PADDING, maxX),
          y: clamp(current.y + deltaY, VIEWPORT_PADDING, maxY),
        };

        if (minimized) {
          minimizedPositionRef.current = nextPosition;
        }

        return nextPosition;
      });
    };

    const finishPointerDrag = (event: PointerEvent) => {
      const dragState = dragStateRef.current;

      if (!dragState || dragState.pointerId !== event.pointerId) {
        return;
      }

      if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
        dragState.captureTarget.releasePointerCapture(event.pointerId);
      }

      dragStateRef.current = null;
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', finishPointerDrag);
    window.addEventListener('pointercancel', finishPointerDrag);

    return () => {
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', finishPointerDrag);
      window.removeEventListener('pointercancel', finishPointerDrag);
    };
  }, [isDesktopMobileShell, minimized]);

  useEffect(() => {
    if (isDesktopMobileShell) {
      const { width, height } = getMobileShellMetrics();
      const nextPosition = {
        x: clamp(
          mobileShellPositionRef.current.x,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING),
        ),
        y: clamp(
          mobileShellPositionRef.current.y,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING),
        ),
      };

      mobileShellPositionRef.current = nextPosition;
      setPosition(nextPosition);
    }
  }, [isDesktopMobileShell]);

  useEffect(() => {
    if (!showDetachedConsole) {
      detachedConsoleDragStateRef.current = null;
      detachedConsoleResizeStateRef.current = null;
      clearDetachedConsoleDraggingState();
      return;
    }

    const resizeListener = () => {
      const { width, height } = normalizeDetachedConsoleSize(detachedConsoleSizeRef.current);

      setDetachedConsolePosition((current) => {
        const nextPosition = {
          x: clamp(
            current.x,
            VIEWPORT_PADDING,
            Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING),
          ),
          y: clamp(
            current.y,
            VIEWPORT_PADDING,
            Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING),
          ),
        };

        detachedConsolePositionRef.current = nextPosition;
        return nextPosition;
      });
    };

    window.addEventListener('resize', resizeListener);
    resizeListener();

    return () => {
      window.removeEventListener('resize', resizeListener);
    };
  }, [showDetachedConsole]);

  useEffect(() => {
    if (!showDetachedConsole) {
      return;
    }

    const handlePointerMove = (event: PointerEvent) => {
      const dragState = detachedConsoleDragStateRef.current;
      const resizeState = detachedConsoleResizeStateRef.current;

      if (dragState && dragState.pointerId === event.pointerId) {
        event.preventDefault();

        const deltaX = event.clientX - dragState.lastX;
        const deltaY = event.clientY - dragState.lastY;

        dragState.lastX = event.clientX;
        dragState.lastY = event.clientY;

        const { width, height } = normalizeDetachedConsoleSize(detachedConsoleSizeRef.current);

        setDetachedConsolePosition((current) => {
          const nextPosition = {
            x: clamp(
              current.x + deltaX,
              VIEWPORT_PADDING,
              Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING),
            ),
            y: clamp(
              current.y + deltaY,
              VIEWPORT_PADDING,
              Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING),
            ),
          };

          detachedConsolePositionRef.current = nextPosition;
          return nextPosition;
        });
        return;
      }

      if (resizeState && resizeState.pointerId === event.pointerId) {
        event.preventDefault();

        updateDetachedConsoleSize({
          width: resizeState.startWidth + (event.clientX - resizeState.startX),
          height: resizeState.startHeight + (event.clientY - resizeState.startY),
        });
      }
    };

    const finishPointerDrag = (event: PointerEvent) => {
      const dragState = detachedConsoleDragStateRef.current;
      const resizeState = detachedConsoleResizeStateRef.current;

      if (dragState?.pointerId === event.pointerId) {
        if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
          dragState.captureTarget.releasePointerCapture(event.pointerId);
        }

        detachedConsoleDragStateRef.current = null;
      }

      if (resizeState?.pointerId === event.pointerId) {
        if (resizeState.captureTarget.hasPointerCapture(event.pointerId)) {
          resizeState.captureTarget.releasePointerCapture(event.pointerId);
        }

        detachedConsoleResizeStateRef.current = null;
      }
      clearDetachedConsoleDraggingState();
    };

    window.addEventListener('pointermove', handlePointerMove);
    window.addEventListener('pointerup', finishPointerDrag);
    window.addEventListener('pointercancel', finishPointerDrag);

    return () => {
      window.removeEventListener('pointermove', handlePointerMove);
      window.removeEventListener('pointerup', finishPointerDrag);
      window.removeEventListener('pointercancel', finishPointerDrag);
    };
  }, [showDetachedConsole]);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.origin !== previewOrigin) {
        return;
      }

      if (event.source !== iframeRef.current?.contentWindow) {
        return;
      }

      const payload = event.data as PreviewRuntimeConsoleBridgeMessage | null;

      if (!payload || payload.source !== 'sm-home.preview-runtime.console' || !Array.isArray(payload.args)) {
        return;
      }

      const nextEntry: PreviewConsoleEntry = {
        ...payload,
        id: `${payload.timestamp}-${Math.random().toString(36).slice(2, 10)}`,
      };

      setConsoleEntries((current) => {
        const nextEntries = [...current, nextEntry];

        if (nextEntries.length <= MAX_CONSOLE_ENTRIES) {
          return nextEntries;
        }

        return nextEntries.slice(nextEntries.length - MAX_CONSOLE_ENTRIES);
      });
    };

    window.addEventListener('message', handleMessage);

    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, [previewOrigin]);

  useEffect(() => {
    if (!consoleOpen) {
      return;
    }

    const nextFrame = window.requestAnimationFrame(() => {
      const container = consoleBodyRef.current;

      if (!container) {
        return;
      }

      container.scrollTop = container.scrollHeight;
    });

    return () => {
      window.cancelAnimationFrame(nextFrame);
    };
  }, [consoleEntries, consoleOpen]);

  const latestConsoleEntry = consoleEntries.length > 0 ? consoleEntries[consoleEntries.length - 1] : null;
  const latestConsoleHref = latestConsoleEntry?.href?.trim() || iframeRef.current?.src || '';
  const consoleLevelCounts = useMemo(
    () =>
      CONSOLE_LEVELS.reduce<Record<PreviewRuntimeConsoleLevel, number>>(
        (counts, level) => ({
          ...counts,
          [level]: consoleEntries.filter((entry) => entry.level === level).length,
        }),
        {
          log: 0,
          info: 0,
          warn: 0,
          error: 0,
          debug: 0,
        },
      ),
    [consoleEntries],
  );
  const filteredConsoleEntries = useMemo(
    () => consoleEntries.filter((entry) => consoleLevelFilter.includes(entry.level)),
    [consoleEntries, consoleLevelFilter],
  );
  const errorEntryCount = consoleLevelCounts.error;
  const warnEntryCount = consoleLevelCounts.warn;
  const hasAllConsoleLevelsSelected = consoleLevelFilter.length === CONSOLE_LEVELS.length;
  const hasAnyConsoleLevelsSelected = consoleLevelFilter.length > 0;

  const handleHeaderPointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (!minimized && !isDesktopMobileShell) {
      return;
    }

    const rootRect = rootRef.current?.getBoundingClientRect();

    if (!rootRect) {
      return;
    }

    dragStateRef.current = {
      pointerId: event.pointerId,
      lastX: event.clientX,
      lastY: event.clientY,
      captureTarget: event.currentTarget,
    };
    dragMovedRef.current = false;

    event.currentTarget.setPointerCapture(event.pointerId);
    event.preventDefault();
  };

  const handleHeaderPointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
    const dragState = dragStateRef.current;

    if (dragState?.pointerId === event.pointerId) {
      if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
        dragState.captureTarget.releasePointerCapture(event.pointerId);
      }

      dragStateRef.current = null;
    }
  };

  const handleMinimizeToggle = () => {
    setMinimized((current) => {
      if (current) {
        if (deviceMode === 'mobile' && !isMobileViewport) {
          setPosition(mobileShellPositionRef.current);
        }
        return false;
      }

      if (isDesktopMobileShell) {
        mobileShellPositionRef.current = position;
      }
      setPosition(minimizedPositionRef.current);
      return true;
    });
  };

  const handleActionButtonClick = (event: ReactMouseEvent<HTMLElement>) => {
    event.stopPropagation();
  };

  const handleDetachedConsolePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (!showDetachedConsole || event.button !== 0) {
      return;
    }

    detachedConsoleDragStateRef.current = {
      pointerId: event.pointerId,
      lastX: event.clientX,
      lastY: event.clientY,
      captureTarget: event.currentTarget,
    };

    document.body.classList.add(DETACHED_CONSOLE_DRAGGING_CLASS);
    event.currentTarget.setPointerCapture(event.pointerId);
    event.stopPropagation();
    event.preventDefault();
  };

  const handleDetachedConsolePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
    const dragState = detachedConsoleDragStateRef.current;

    if (dragState?.pointerId === event.pointerId) {
      if (dragState.captureTarget.hasPointerCapture(event.pointerId)) {
        dragState.captureTarget.releasePointerCapture(event.pointerId);
      }

      detachedConsoleDragStateRef.current = null;
    }

    clearDetachedConsoleDraggingState();
    event.stopPropagation();
  };

  const handleDetachedConsoleResizePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
    if (!showDetachedConsole || event.button !== 0) {
      return;
    }

    detachedConsoleResizeStateRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      startWidth: detachedConsoleSizeRef.current.width,
      startHeight: detachedConsoleSizeRef.current.height,
      captureTarget: event.currentTarget,
    };

    document.body.classList.add(DETACHED_CONSOLE_DRAGGING_CLASS);
    event.currentTarget.setPointerCapture(event.pointerId);
    event.stopPropagation();
    event.preventDefault();
  };

  const handleDetachedConsoleResizePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
    const resizeState = detachedConsoleResizeStateRef.current;

    if (resizeState?.pointerId === event.pointerId) {
      if (resizeState.captureTarget.hasPointerCapture(event.pointerId)) {
        resizeState.captureTarget.releasePointerCapture(event.pointerId);
      }

      detachedConsoleResizeStateRef.current = null;
    }

    clearDetachedConsoleDraggingState();
    event.stopPropagation();
  };

  const updateDetachedConsoleSize = (nextSize: DetachedConsoleSize) => {
    const normalized = normalizeDetachedConsoleSize(nextSize);
    detachedConsoleSizeRef.current = normalized;
    setDetachedConsoleSize(normalized);
    setDetachedConsolePosition((current) => {
      const nextPosition = {
        x: clamp(
          current.x,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerWidth - normalized.width - VIEWPORT_PADDING),
        ),
        y: clamp(
          current.y,
          VIEWPORT_PADDING,
          Math.max(VIEWPORT_PADDING, window.innerHeight - normalized.height - VIEWPORT_PADDING),
        ),
      };

      detachedConsolePositionRef.current = nextPosition;
      return nextPosition;
    });
  };

  const toggleConsoleLevel = (level: PreviewRuntimeConsoleLevel) => {
    setConsoleLevelFilter((current) => {
      if (current.includes(level)) {
        if (current.length === 1) {
          return current;
        }
        return current.filter((item) => item !== level);
      }

      return CONSOLE_LEVELS.filter((item) => item === level || current.includes(item));
    });
  };

  const renderConsolePanel = (detached: boolean) => (
    <section
      className={`preview-app-overlay__console-panel${
        detached ? ' preview-app-overlay__console-panel--detached' : ''
      }`}
      aria-label="Preview 콘솔"
      style={
        detached
          ? {
              width: `${detachedConsoleSize.width}px`,
              height: `${detachedConsoleSize.height}px`,
              left: `${detachedConsolePosition.x}px`,
              top: `${detachedConsolePosition.y}px`,
            }
          : undefined
      }
    >
      <div
        className={`preview-app-overlay__console-head${
          detached ? ' preview-app-overlay__console-head--detached' : ''
        }`}
        onPointerDown={detached ? handleDetachedConsolePointerDown : undefined}
        onPointerUp={detached ? handleDetachedConsolePointerUp : undefined}
        onPointerCancel={detached ? handleDetachedConsolePointerUp : undefined}
      >
        <div className="preview-app-overlay__console-head-copy">
          <strong>{detached ? 'Console Detached' : 'Console'}</strong>
          <div className="preview-app-overlay__console-summary" aria-label="Preview 콘솔 요약">
            <span>로그 {consoleEntries.length}</span>
            <span>경고 {warnEntryCount}</span>
            <span>오류 {errorEntryCount}</span>
          </div>
          <div className="preview-app-overlay__console-location" title={latestConsoleHref || 'Preview URL 확인 대기 중'}>
            {latestConsoleHref || 'Preview URL 확인 대기 중'}
          </div>
        </div>
        <div
          className="preview-app-overlay__console-head-actions"
          onPointerDown={(event) => {
            event.stopPropagation();
          }}
        >
          {canDetachConsole ? (
            <Button
              type="text"
              size="small"
              aria-label={detached ? '콘솔 오버레이에 붙이기' : '콘솔 분리하기'}
              onClick={() => {
                setConsoleDetached((current) => !current);
              }}
            >
              {detached ? 'Attach' : 'Detach'}
            </Button>
          ) : null}
          <Button
            type="text"
            size="small"
            icon={<CopyOutlined />}
            aria-label="Preview URL 복사"
            disabled={!latestConsoleHref}
            onClick={() => {
              if (!latestConsoleHref) {
                return;
              }

              void copyTextToClipboard(latestConsoleHref);
            }}
          >
            Copy URL
          </Button>
          <Button
            type="text"
            size="small"
            aria-label="Preview 콘솔 비우기"
            onClick={() => {
              setConsoleEntries([]);
            }}
          >
            Clear
          </Button>
          {detached ? (
            <Button
              type="text"
              size="small"
              aria-label="Preview 콘솔 닫기"
              onClick={() => {
                setConsoleOpen(false);
              }}
            >
              Close
            </Button>
          ) : null}
        </div>
      </div>
      <div className="preview-app-overlay__console-filters" aria-label="Preview 콘솔 레벨 필터">
        <Button
          type="text"
          size="small"
          className={`preview-app-overlay__console-filter${hasAllConsoleLevelsSelected ? ' is-active' : ''}`}
          aria-pressed={hasAllConsoleLevelsSelected}
          onClick={() => {
            setConsoleLevelFilter(CONSOLE_LEVELS);
          }}
        >
          All {consoleEntries.length}
        </Button>
        {CONSOLE_LEVELS.map((level) => (
          <Button
            key={level}
            type="text"
            size="small"
            className={`preview-app-overlay__console-filter preview-app-overlay__console-filter--${level}${
              consoleLevelFilter.includes(level) ? ' is-active' : ''
            }`}
            aria-pressed={consoleLevelFilter.includes(level)}
            onClick={() => {
              toggleConsoleLevel(level);
            }}
          >
            {level.toUpperCase()} {consoleLevelCounts[level]}
          </Button>
        ))}
      </div>
      <div ref={consoleBodyRef} className="preview-app-overlay__console-body">
        {filteredConsoleEntries.length ? (
          filteredConsoleEntries.map((entry) => {
            const entryTimeLabel = new Date(entry.timestamp).toLocaleTimeString('ko-KR', {
              hour12: false,
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
            });

            return (
              <article
                key={entry.id}
                className={`preview-app-overlay__console-entry preview-app-overlay__console-entry--${entry.level}`}
              >
                <div className="preview-app-overlay__console-meta">
                  <div className="preview-app-overlay__console-meta-copy">
                    <span>{entry.level.toUpperCase()}</span>
                    <time dateTime={entry.timestamp}>{entryTimeLabel}</time>
                  </div>
                  <Button
                    type="text"
                    size="small"
                    className="preview-app-overlay__console-entry-copy"
                    icon={<CopyOutlined />}
                    aria-label="콘솔 항목 복사"
                    onClick={() => {
                      void copyTextToClipboard(formatConsoleEntryCopyText(entry));
                    }}
                  />
                </div>
                <pre>{entry.args.join('\n')}</pre>
              </article>
            );
          })
        ) : (
          <div className="preview-app-overlay__console-empty">
            {consoleEntries.length && !hasAnyConsoleLevelsSelected
              ? '표시할 콘솔 레벨이 선택되지 않았습니다.'
              : consoleEntries.length
                ? '선택한 레벨에 해당하는 콘솔 로그가 아직 없습니다.'
                : 'iframe 콘솔 로그가 아직 없습니다.'}
            <br />
            {consoleEntries.length
              ? '필터를 조정하거나 Preview 앱에서 동작을 다시 재현해 주세요.'
              : 'Preview 앱에서 동작을 재현하면 이 패널에서 바로 확인할 수 있습니다.'}
          </div>
        )}
      </div>
      {detached ? (
        <div
          className="preview-app-overlay__console-resize-handle"
          aria-label="분리 콘솔 크기 조절"
          role="presentation"
          onPointerDown={handleDetachedConsoleResizePointerDown}
          onPointerUp={handleDetachedConsoleResizePointerUp}
          onPointerCancel={handleDetachedConsoleResizePointerUp}
        />
      ) : null}
    </section>
  );

  return createPortal(
    <>
      <div
        ref={rootRef}
        className={`preview-app-overlay${minimized ? ' preview-app-overlay--minimized' : ''}${
          isDesktopMobileShell ? ' preview-app-overlay--mobile-shell' : ''
        }`}
        style={
          minimized || isDesktopMobileShell
            ? {
                left: `${position.x}px`,
                top: `${position.y}px`,
              }
            : undefined
        }
      >
        <div
          className="preview-app-overlay__header"
          onClick={() => {
            if (minimized && !dragMovedRef.current) {
              setMinimized(false);
            }
          }}
          onPointerDown={handleHeaderPointerDown}
          onPointerUp={handleHeaderPointerUp}
          onPointerCancel={handleHeaderPointerUp}
        >
          <div className={`preview-app-overlay__title${minimized ? ' preview-app-overlay__title--minimized' : ''}`}>
            {minimized ? (
              <div className="preview-app-overlay__minimized-content">
                <span className="preview-app-overlay__minimized-dot" aria-hidden="true" />
                <span className="preview-app-overlay__minimized-label">Preview App</span>
              </div>
            ) : (
              <>
                <span className="preview-app-overlay__title-badge" aria-hidden="true" />
                <span className="preview-app-overlay__title-copy">
                  <strong>Preview App</strong>
                  <span>모바일 컨테이너 미리보기</span>
                </span>
              </>
            )}
          </div>
          <div className="preview-app-overlay__actions">
            {!minimized && !isMobileViewport ? (
              <Button
                type="text"
                aria-label={deviceMode === 'mobile' ? '전체 폭으로 보기' : '모바일 폭으로 보기'}
                title={deviceMode === 'mobile' ? '전체 폭으로 보기' : '모바일 폭으로 보기'}
                icon={deviceMode === 'mobile' ? <DesktopOutlined /> : <MobileOutlined />}
                onClick={(event) => {
                  handleActionButtonClick(event);
                  setDeviceMode((current) => (current === 'mobile' ? 'desktop' : 'mobile'));
                }}
              />
            ) : null}
            {!minimized ? (
              <Button
                type="text"
                aria-label="Preview 새로고침"
                title="Preview 새로고침"
                icon={<ReloadOutlined />}
                onClick={(event) => {
                  handleActionButtonClick(event);
                  setConsoleEntries([]);
                  setReloadKey((current) => current + 1);
                }}
              />
            ) : null}
            {!minimized ? (
              <Button
                type="text"
                aria-label={consoleOpen ? 'Preview 콘솔 닫기' : 'Preview 콘솔 보기'}
                title={consoleOpen ? 'Preview 콘솔 닫기' : 'Preview 콘솔 보기'}
                className={`preview-app-overlay__console-toggle${consoleEntries.length ? ' preview-app-overlay__console-toggle--active' : ''}`}
                icon={<CodeOutlined />}
                onClick={(event) => {
                  handleActionButtonClick(event);
                  setConsoleOpen((current) => !current);
                }}
              >
                Console
              </Button>
            ) : null}
            {!minimized ? (
              <Button
                type="text"
                aria-label="Preview 최소화"
                icon={<MinusOutlined />}
                onClick={(event) => {
                  handleActionButtonClick(event);
                  handleMinimizeToggle();
                }}
              />
            ) : null}
            <Button
              type="text"
              danger
              aria-label="Preview 닫기"
              icon={<CloseOutlined />}
              onClick={(event) => {
                handleActionButtonClick(event);
                dragMovedRef.current = false;
                onClose();
              }}
            />
          </div>
        </div>
        <div className={`preview-app-overlay__body${minimized ? ' preview-app-overlay__body--hidden' : ''}`}>
          <PreviewAppWindow
            ref={iframeRef}
            pathname={pathname}
            search={search}
            targetDescriptor={targetDescriptor}
            deviceMode={deviceMode}
            reloadKey={reloadKey}
          />
          {showAttachedConsole ? renderConsolePanel(false) : null}
        </div>
      </div>
      {showDetachedConsole ? renderConsolePanel(true) : null}
    </>,
    document.body,
  );
}
