"use client";

import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  ChevronLeft,
  ChevronRight,
  Calendar,
  Plus,
  Loader2,
  ZoomIn,
  ZoomOut,
  Package,
  Zap,
  RefreshCw,
  Download,
  Upload,
  Play,
  FileText,
  Send,
  Sparkles,
  Wand2,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
  TimelineSchedulerComponentProps,
  ScheduleItem,
  ZoomLevel,
  ToolbarAction,
} from "./types";
import { useTimelineData } from "./hooks/useTimelineData";
import { TimelineHeader, ResourceRow, TimelineLegend, ItemTimelineCard, groupSchedulesByItem, SchedulePreviewDialog } from "./components";
import { zoomLevelOptions, defaultTimelineSchedulerConfig, statusOptions } from "./config";
import { detectConflicts, addDaysToDateString } from "./utils/conflictDetection";
import { v2EventBus, V2_EVENTS } from "@/lib/v2-core";
import { apiClient } from "@/lib/api/client";

// 가상 스크롤 활성화 임계값 (리소스 수)
const VIRTUAL_THRESHOLD = 30;

/**
 * v2-timeline-scheduler 메인 컴포넌트
 *
 * 간트차트 형태의 일정/계획 시각화 및 편집 컴포넌트
 */
export function TimelineSchedulerComponent({
  config,
  isDesignMode = false,
  formData,
  externalSchedules,
  externalResources,
  isLoading: externalLoading,
  error: externalError,
  componentId,
  onDragEnd,
  onResizeEnd,
  onScheduleClick,
  onCellClick,
  onAddSchedule,
}: TimelineSchedulerComponentProps) {
  const containerRef = useRef<HTMLDivElement>(null);

  // ────────── 툴바 액션 다이얼로그 상태 (통합) ──────────
  const [actionDialog, setActionDialog] = useState<{
    actionId: string;
    action: ToolbarAction;
    isLoading: boolean;
    isApplying: boolean;
    summary: any;
    previews: any[];
    deletedSchedules: any[];
    keptSchedules: any[];
    preparedPayload: any;
  } | null>(null);
  const linkedFilterValuesRef = useRef<any[]>([]);

  // ────────── 아이콘 맵 ──────────
  const TOOLBAR_ICONS: Record<string, React.ComponentType<{ className?: string }>> = useMemo(() => ({
    Zap, Package, Plus, Download, Upload, RefreshCw, Play, FileText, Send, Sparkles, Wand2,
  }), []);

  // ────────── linkedFilter 상태 ──────────
  const linkedFilter = config.linkedFilter;
  const hasLinkedFilter = !!linkedFilter;
  const [linkedFilterValues, setLinkedFilterValues] = useState<string[]>([]);
  const [hasReceivedSelection, setHasReceivedSelection] = useState(false);

  // linkedFilter 이벤트 수신
  useEffect(() => {
    if (!hasLinkedFilter) return;

    const handler = (event: any) => {
      if (linkedFilter!.sourceTableName && event.tableName !== linkedFilter!.sourceTableName) return;
      if (linkedFilter!.sourceComponentId && event.componentId !== linkedFilter!.sourceComponentId) return;

      const selectedRows: any[] = event.selectedRows || [];
      const sourceField = linkedFilter!.sourceField;

      const values = selectedRows
        .map((row: any) => String(row[sourceField] ?? ""))
        .filter((v: string) => v !== "" && v !== "undefined" && v !== "null");

      const uniqueValues = [...new Set(values)];
      setLinkedFilterValues(uniqueValues);
      setHasReceivedSelection(true);
      linkedFilterValuesRef.current = selectedRows;
    };

    const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_SELECTION_CHANGE, handler);
    return unsubscribe;
  }, [hasLinkedFilter, linkedFilter]);

  // 타임라인 데이터 훅
  const {
    schedules: rawSchedules,
    resources,
    isLoading: hookLoading,
    error: hookError,
    zoomLevel,
    setZoomLevel,
    viewStartDate,
    viewEndDate,
    goToPrevious,
    goToNext,
    goToToday,
    updateSchedule,
    refresh: refreshTimeline,
  } = useTimelineData(config, externalSchedules, externalResources);

  // linkedFilter 적용: 선택된 값으로 스케줄 필터링
  const schedules = useMemo(() => {
    if (!hasLinkedFilter) return rawSchedules;
    if (linkedFilterValues.length === 0) return [];

    const targetField = linkedFilter!.targetField;
    return rawSchedules.filter((s) => {
      const val = String((s.data as any)?.[targetField] ?? (s as any)[targetField] ?? "");
      return linkedFilterValues.includes(val);
    });
  }, [rawSchedules, hasLinkedFilter, linkedFilterValues, linkedFilter]);

  const isLoading = externalLoading ?? hookLoading;
  const error = externalError ?? hookError;

  // 설정값
  const rowHeight =
    config.rowHeight || defaultTimelineSchedulerConfig.rowHeight!;
  const headerHeight =
    config.headerHeight || defaultTimelineSchedulerConfig.headerHeight!;
  const resourceColumnWidth =
    config.resourceColumnWidth ||
    defaultTimelineSchedulerConfig.resourceColumnWidth!;
  const cellWidthConfig =
    config.cellWidth || defaultTimelineSchedulerConfig.cellWidth!;
  const cellWidth = cellWidthConfig[zoomLevel] || 60;

  // 리소스 자동 생성 (리소스 테이블 미설정 시 스케줄 데이터에서 추출)
  const effectiveResources = useMemo(() => {
    if (resources.length > 0) return resources;

    const uniqueResourceIds = new Set<string>();
    schedules.forEach((s) => {
      if (s.resourceId) uniqueResourceIds.add(s.resourceId);
    });

    return Array.from(uniqueResourceIds).map((id) => ({ id, name: id }));
  }, [resources, schedules]);

  // 리소스별 스케줄 그룹화
  const schedulesByResource = useMemo(() => {
    const grouped = new Map<string, ScheduleItem[]>();

    effectiveResources.forEach((r) => grouped.set(r.id, []));

    schedules.forEach((schedule) => {
      const list = grouped.get(schedule.resourceId);
      if (list) {
        list.push(schedule);
      } else {
        const firstResource = effectiveResources[0];
        if (firstResource) {
          grouped.get(firstResource.id)?.push(schedule);
        }
      }
    });

    return grouped;
  }, [schedules, effectiveResources]);

  // ────────── 충돌 감지 ──────────
  const conflictIds = useMemo(() => {
    if (config.showConflicts === false) return new Set<string>();
    return detectConflicts(schedules);
  }, [schedules, config.showConflicts]);

  // ────────── 줌 레벨 변경 ──────────
  const handleZoomIn = useCallback(() => {
    const levels: ZoomLevel[] = ["month", "week", "day"];
    const idx = levels.indexOf(zoomLevel);
    if (idx < levels.length - 1) setZoomLevel(levels[idx + 1]);
  }, [zoomLevel, setZoomLevel]);

  const handleZoomOut = useCallback(() => {
    const levels: ZoomLevel[] = ["month", "week", "day"];
    const idx = levels.indexOf(zoomLevel);
    if (idx > 0) setZoomLevel(levels[idx - 1]);
  }, [zoomLevel, setZoomLevel]);

  // ────────── 스케줄 클릭 ──────────
  const handleScheduleClick = useCallback(
    (schedule: ScheduleItem) => {
      const resource = effectiveResources.find(
        (r) => r.id === schedule.resourceId
      );
      if (resource && onScheduleClick) {
        onScheduleClick({ schedule, resource });
      }
    },
    [effectiveResources, onScheduleClick]
  );

  // ────────── 빈 셀 클릭 ──────────
  const handleCellClick = useCallback(
    (resourceId: string, date: Date) => {
      if (onCellClick) {
        onCellClick({
          resourceId,
          date: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`,
        });
      }
    },
    [onCellClick]
  );

  // ────────── 드래그 완료 (핵심 로직) ──────────
  const handleDragComplete = useCallback(
    async (schedule: ScheduleItem, deltaX: number) => {
      // 줌 레벨에 따라 1셀당 일수가 달라짐
      let daysPerCell = 1;
      if (zoomLevel === "week") daysPerCell = 7;
      if (zoomLevel === "month") daysPerCell = 30;

      const deltaDays = Math.round((deltaX / cellWidth) * daysPerCell);
      if (deltaDays === 0) return;

      const newStartDate = addDaysToDateString(schedule.startDate, deltaDays);
      const newEndDate = addDaysToDateString(schedule.endDate, deltaDays);

      try {
        await updateSchedule(schedule.id, {
          startDate: newStartDate,
          endDate: newEndDate,
        });

        // 외부 이벤트 핸들러 호출
        onDragEnd?.({
          scheduleId: schedule.id,
          newStartDate,
          newEndDate,
        });

        toast.success("스케줄 이동 완료", {
          description: `${schedule.title}: ${newStartDate} ~ ${newEndDate}`,
        });
      } catch (err: any) {
        toast.error("스케줄 이동 실패", {
          description: err.message || "잠시 후 다시 시도해주세요",
        });
      }
    },
    [cellWidth, zoomLevel, updateSchedule, onDragEnd]
  );

  // ────────── 리사이즈 완료 (핵심 로직) ──────────
  const handleResizeComplete = useCallback(
    async (
      schedule: ScheduleItem,
      direction: "start" | "end",
      deltaX: number
    ) => {
      let daysPerCell = 1;
      if (zoomLevel === "week") daysPerCell = 7;
      if (zoomLevel === "month") daysPerCell = 30;

      const deltaDays = Math.round((deltaX / cellWidth) * daysPerCell);
      if (deltaDays === 0) return;

      let newStartDate = schedule.startDate;
      let newEndDate = schedule.endDate;

      if (direction === "start") {
        newStartDate = addDaysToDateString(schedule.startDate, deltaDays);
        // 시작일이 종료일을 넘지 않도록
        if (new Date(newStartDate) >= new Date(newEndDate)) {
          toast.warning("시작일은 종료일보다 이전이어야 합니다");
          return;
        }
      } else {
        newEndDate = addDaysToDateString(schedule.endDate, deltaDays);
        // 종료일이 시작일보다 앞서지 않도록
        if (new Date(newEndDate) <= new Date(newStartDate)) {
          toast.warning("종료일은 시작일보다 이후여야 합니다");
          return;
        }
      }

      try {
        await updateSchedule(schedule.id, {
          startDate: newStartDate,
          endDate: newEndDate,
        });

        onResizeEnd?.({
          scheduleId: schedule.id,
          newStartDate,
          newEndDate,
          direction,
        });

        const days =
          Math.round(
            (new Date(newEndDate).getTime() -
              new Date(newStartDate).getTime()) /
              (1000 * 60 * 60 * 24)
          ) + 1;

        toast.success("기간 변경 완료", {
          description: `${schedule.title}: ${days}일 (${newStartDate} ~ ${newEndDate})`,
        });
      } catch (err: any) {
        toast.error("기간 변경 실패", {
          description: err.message || "잠시 후 다시 시도해주세요",
        });
      }
    },
    [cellWidth, zoomLevel, updateSchedule, onResizeEnd]
  );

  // ────────── 추가 버튼 클릭 ──────────
  const handleAddClick = useCallback(() => {
    if (onAddSchedule && effectiveResources.length > 0) {
      onAddSchedule(
        effectiveResources[0].id,
        (() => { const _n = new Date(); return `${_n.getFullYear()}-${String(_n.getMonth() + 1).padStart(2, "0")}-${String(_n.getDate()).padStart(2, "0")}`; })()
      );
    }
  }, [onAddSchedule, effectiveResources]);

  // ────────── 유효 툴바 액션 (config 기반 또는 하위호환 자동생성) ──────────
  const effectiveToolbarActions: ToolbarAction[] = useMemo(() => {
    if (config.toolbarActions && config.toolbarActions.length > 0) {
      return config.toolbarActions;
    }
    return [];
  }, [config.toolbarActions]);

  // ────────── 범용 액션: 미리보기 요청 ──────────
  const handleActionPreview = useCallback(async (action: ToolbarAction) => {
    let payload: any;

    if (action.dataSource === "linkedSelection") {
      const selectedRows = linkedFilterValuesRef.current;
      if (!selectedRows || selectedRows.length === 0) {
        toast.warning("좌측에서 항목을 선택해주세요");
        return;
      }

      const groupField = action.payloadConfig?.groupByField || config.linkedFilter?.sourceField || "part_code";
      const qtyField = action.payloadConfig?.quantityField || config.sourceConfig?.quantityField || "balance_qty";
      const dateField = action.payloadConfig?.dueDateField || config.sourceConfig?.dueDateField || "due_date";
      const nameField = action.payloadConfig?.nameField || config.sourceConfig?.groupNameField || "part_name";

      const grouped = new Map<string, any[]>();
      selectedRows.forEach((row: any) => {
        const key = row[groupField] || "";
        if (!key) return;
        if (!grouped.has(key)) grouped.set(key, []);
        grouped.get(key)!.push(row);
      });

      const items = Array.from(grouped.entries()).map(([code, rows]) => {
        const totalQty = rows.reduce((sum: number, r: any) => sum + (Number(r[qtyField]) || 0), 0);
        const dates = rows.map((r: any) => r[dateField]).filter(Boolean).sort();
        const _dn = new Date();
        const earliestDate = dates[0] || `${_dn.getFullYear()}-${String(_dn.getMonth() + 1).padStart(2, "0")}-${String(_dn.getDate()).padStart(2, "0")}`;
        const first = rows[0];
        return {
          item_code: code,
          item_name: first[nameField] || first.item_name || code,
          required_qty: totalQty,
          earliest_due_date: typeof earliestDate === "string" ? earliestDate.split("T")[0] : earliestDate,
          hourly_capacity: Number(first.hourly_capacity) || undefined,
          daily_capacity: Number(first.daily_capacity) || undefined,
        };
      }).filter((item) => item.required_qty > 0);

      if (items.length === 0) {
        toast.warning("선택된 항목의 잔량이 없습니다");
        return;
      }

      payload = {
        items,
        options: {
          ...(config.staticFilters || {}),
          ...(action.payloadConfig?.extraOptions || {}),
        },
      };
    } else if (action.dataSource === "currentSchedules") {
      let targetSchedules = schedules;
      const filterField = action.payloadConfig?.scheduleFilterField;
      const filterValue = action.payloadConfig?.scheduleFilterValue;

      if (filterField && filterValue) {
        targetSchedules = schedules.filter((s) => {
          const val = (s.data as any)?.[filterField] || "";
          return val === filterValue;
        });
      }

      if (targetSchedules.length === 0) {
        toast.warning("대상 스케줄이 없습니다");
        return;
      }

      const planIds = targetSchedules.map((s) => Number(s.id)).filter((id) => !isNaN(id));
      if (planIds.length === 0) {
        toast.warning("유효한 스케줄 ID가 없습니다");
        return;
      }

      payload = {
        plan_ids: planIds,
        options: action.payloadConfig?.extraOptions || {},
      };
    }

    setActionDialog({
      actionId: action.id,
      action,
      isLoading: true,
      isApplying: false,
      summary: null,
      previews: [],
      deletedSchedules: [],
      keptSchedules: [],
      preparedPayload: payload,
    });

    try {
      const response = await apiClient.post(action.previewApi, payload);
      if (response.data?.success) {
        setActionDialog((prev) => prev ? {
          ...prev,
          isLoading: false,
          summary: response.data.data.summary,
          previews: response.data.data.previews || [],
          deletedSchedules: response.data.data.deletedSchedules || [],
          keptSchedules: response.data.data.keptSchedules || [],
        } : null);
      } else {
        toast.error("미리보기 생성 실패", { description: response.data?.message });
        setActionDialog(null);
      }
    } catch (err: any) {
      toast.error("미리보기 요청 실패", { description: err.message });
      setActionDialog(null);
    }
  }, [config.linkedFilter, config.staticFilters, config.sourceConfig, schedules]);

  // ────────── 범용 액션: 확인 및 적용 ──────────
  const handleActionApply = useCallback(async () => {
    if (!actionDialog) return;
    const { action, preparedPayload } = actionDialog;

    setActionDialog((prev) => prev ? { ...prev, isApplying: true } : null);

    try {
      const response = await apiClient.post(action.applyApi, preparedPayload);
      if (response.data?.success) {
        const data = response.data.data;
        const summary = data.summary || data;
        toast.success(action.dialogTitle || "완료", {
          description: `신규: ${summary.new_count || summary.count || 0}건${summary.kept_count ? `, 유지: ${summary.kept_count}건` : ""}${summary.deleted_count ? `, 삭제: ${summary.deleted_count}건` : ""}`,
        });
        setActionDialog(null);
        refreshTimeline();
      } else {
        toast.error("실행 실패", { description: response.data?.message });
      }
    } catch (err: any) {
      toast.error("실행 실패", { description: err.message });
    } finally {
      setActionDialog((prev) => prev ? { ...prev, isApplying: false } : null);
    }
  }, [actionDialog, refreshTimeline]);

  // ────────── 하단 영역 높이 계산 (툴바 + 범례) ──────────
  const showToolbar = config.showToolbar !== false;
  const showLegend = config.showLegend !== false;

  // ────────── 가상 스크롤 ──────────
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const useVirtual = effectiveResources.length >= VIRTUAL_THRESHOLD;

  const virtualizer = useVirtualizer({
    count: effectiveResources.length,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: () => rowHeight,
    overscan: 5,
  });

  // ────────── 디자인 모드 플레이스홀더 ──────────
  if (isDesignMode) {
    return (
      <div className="flex h-full min-h-[200px] w-full items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/30 bg-muted/10">
        <div className="text-center text-muted-foreground">
          <Calendar className="mx-auto mb-2 h-6 w-6 sm:h-8 sm:w-8" />
          <p className="text-xs font-medium sm:text-sm">타임라인 스케줄러</p>
          <p className="mt-1 text-[10px] sm:text-xs">
            {config.selectedTable
              ? `테이블: ${config.selectedTable}`
              : "테이블을 선택하세요"}
          </p>
        </div>
      </div>
    );
  }

  // ────────── 로딩 상태 ──────────
  if (isLoading) {
    return (
      <div
        className="flex w-full items-center justify-center rounded-lg bg-muted/10"
        style={{ height: config.height || 500 }}
      >
        <div className="flex items-center gap-2 text-muted-foreground">
          <Loader2 className="h-4 w-4 animate-spin sm:h-5 sm:w-5" />
          <span className="text-xs sm:text-sm">로딩 중...</span>
        </div>
      </div>
    );
  }

  // ────────── 에러 상태 ──────────
  if (error) {
    return (
      <div
        className="flex w-full items-center justify-center rounded-lg bg-destructive/10"
        style={{ height: config.height || 500 }}
      >
        <div className="text-center text-destructive">
          <p className="text-xs font-medium sm:text-sm">오류 발생</p>
          <p className="mt-1 text-[10px] sm:text-xs">{error}</p>
        </div>
      </div>
    );
  }

  // ────────── linkedFilter 빈 상태 (itemGrouped가 아닌 경우만 early return) ──────────
  // itemGrouped 모드에서는 툴바를 항상 보여주기 위해 여기서 return하지 않음
  if (config.viewMode !== "itemGrouped") {
    if (hasLinkedFilter && !hasReceivedSelection) {
      const emptyMsg = linkedFilter?.emptyMessage || "좌측 목록에서 품목 또는 수주를 선택하세요";
      return (
        <div
          className="flex w-full items-center justify-center rounded-lg border bg-muted/10"
          style={{ height: config.height || 500 }}
        >
          <div className="text-center text-muted-foreground">
            <Package className="mx-auto mb-2 h-8 w-8 opacity-30 sm:mb-3 sm:h-10 sm:w-10" />
            <p className="text-xs font-medium sm:text-sm">{emptyMsg}</p>
            <p className="mt-1.5 max-w-[220px] text-[10px] sm:mt-2 sm:text-xs">
              선택한 항목에 대한 생산계획 타임라인이 여기에 표시됩니다
            </p>
          </div>
        </div>
      );
    }

    if (hasLinkedFilter && hasReceivedSelection && schedules.length === 0) {
      return (
        <div
          className="flex w-full items-center justify-center rounded-lg border bg-muted/10"
          style={{ height: config.height || 500 }}
        >
          <div className="text-center text-muted-foreground">
            <Calendar className="mx-auto mb-2 h-8 w-8 opacity-30 sm:mb-3 sm:h-10 sm:w-10" />
            <p className="text-xs font-medium sm:text-sm">
              선택한 항목에 대한 스케줄이 없습니다
            </p>
            <p className="mt-1.5 max-w-[220px] text-[10px] sm:mt-2 sm:text-xs">
              다른 품목을 선택하거나 스케줄을 생성해 주세요
            </p>
          </div>
        </div>
      );
    }
  }

  // ────────── 데이터 없음 (linkedFilter 없고 itemGrouped가 아닌 경우) ──────────
  if (schedules.length === 0 && config.viewMode !== "itemGrouped") {
    return (
      <div
        className="flex w-full items-center justify-center rounded-lg border bg-muted/10"
        style={{ height: config.height || 500 }}
      >
        <div className="text-center text-muted-foreground">
          <Calendar className="mx-auto mb-2 h-8 w-8 opacity-50 sm:mb-3 sm:h-10 sm:w-10" />
          <p className="text-xs font-medium sm:text-sm">
            스케줄 데이터가 없습니다
          </p>
          <p className="mt-1.5 max-w-[200px] text-[10px] sm:mt-2 sm:text-xs">
            좌측 테이블에서 품목을 선택하거나,
            <br />
            스케줄 생성 버튼을 눌러 스케줄을 생성하세요
          </p>
        </div>
      </div>
    );
  }

  // ────────── 품목 그룹 모드 렌더링 ──────────
  if (config.viewMode === "itemGrouped") {
    const itemGroups = groupSchedulesByItem(schedules);

    return (
      <div
        ref={containerRef}
        className="flex w-full flex-col overflow-hidden rounded-lg border bg-background"
        style={{
          height: config.height || 500,
          maxHeight: config.maxHeight,
        }}
      >
        {/* 툴바: 액션 버튼 + 네비게이션 */}
        {showToolbar && (
          <div className="flex shrink-0 flex-wrap items-center gap-1.5 border-b bg-muted/30 px-2 py-1.5 sm:gap-2 sm:px-3 sm:py-2">
            {/* 네비게이션 */}
            <div className="flex items-center gap-0.5 sm:gap-1">
              {config.showNavigation !== false && (
                <>
                  <Button variant="ghost" size="sm" onClick={goToPrevious} className="h-6 px-1.5 sm:h-7 sm:px-2">
                    <ChevronLeft className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                  </Button>
                  <Button variant="ghost" size="sm" onClick={goToToday} className="h-6 px-2 text-[10px] sm:h-7 sm:px-3 sm:text-xs">
                    오늘
                  </Button>
                  <Button variant="ghost" size="sm" onClick={goToNext} className="h-6 px-1.5 sm:h-7 sm:px-2">
                    <ChevronRight className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                  </Button>
                </>
              )}
              <span className="ml-1 text-[10px] text-muted-foreground sm:ml-2 sm:text-xs">
                {viewStartDate.toLocaleDateString("ko-KR")} ~ {viewEndDate.toLocaleDateString("ko-KR")}
              </span>
            </div>

            {/* 줌 + 액션 버튼 */}
            <div className="ml-auto flex items-center gap-1 sm:gap-1.5">
              {config.showZoomControls !== false && (
                <>
                  <Button variant="ghost" size="sm" onClick={handleZoomOut} className="h-6 px-1.5 sm:h-7 sm:px-2">
                    <ZoomOut className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                  </Button>
                  <span className="text-[10px] font-medium sm:text-xs">
                    {zoomLevelOptions.find((o) => o.value === zoomLevel)?.label}
                  </span>
                  <Button variant="ghost" size="sm" onClick={handleZoomIn} className="h-6 px-1.5 sm:h-7 sm:px-2">
                    <ZoomIn className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                  </Button>
                  <div className="mx-0.5 h-4 w-px bg-border" />
                </>
              )}

              <Button variant="outline" size="sm" onClick={refreshTimeline} className="h-6 gap-1 px-2 text-[10px] sm:h-7 sm:px-3 sm:text-xs">
                <RefreshCw className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
                새로고침
              </Button>
              {effectiveToolbarActions.map((action) => {
                if (action.showWhen) {
                  const matches = Object.entries(action.showWhen).every(
                    ([key, value]) => config.staticFilters?.[key] === value
                  );
                  if (!matches) return null;
                }
                const IconComp = TOOLBAR_ICONS[action.icon || "Zap"] || Zap;
                return (
                  <Button
                    key={action.id}
                    size="sm"
                    onClick={() => handleActionPreview(action)}
                    className={cn("h-6 gap-1 px-2 text-[10px] sm:h-7 sm:px-3 sm:text-xs", action.color || "bg-primary hover:bg-primary/90")}
                  >
                    <IconComp className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
                    {action.label}
                  </Button>
                );
              })}
            </div>
          </div>
        )}

        {/* 범례 */}
        {showLegend && (
          <div className="flex shrink-0 flex-wrap items-center gap-3 border-b px-3 py-1.5 sm:gap-4 sm:px-4 sm:py-2">
            <span className="text-[10px] text-muted-foreground sm:text-xs">생산 상태:</span>
            {statusOptions.map((s) => (
              <div key={s.value} className="flex items-center gap-1">
                <div className="h-2.5 w-2.5 rounded-sm sm:h-3 sm:w-3" style={{ backgroundColor: s.color }} />
                <span className="text-[10px] sm:text-xs">{s.label}</span>
              </div>
            ))}
            <span className="text-[10px] text-muted-foreground sm:text-xs">납기:</span>
            <div className="flex items-center gap-1">
              <div className="h-2.5 w-2.5 rounded-sm border-2 border-destructive sm:h-3 sm:w-3" />
              <span className="text-[10px] sm:text-xs">납기일</span>
            </div>
          </div>
        )}

        {/* 품목별 카드 목록 또는 빈 상태 */}
        {itemGroups.length > 0 ? (
          <div className="flex-1 space-y-3 overflow-y-auto p-3 sm:space-y-4 sm:p-4">
            {itemGroups.map((group) => (
              <ItemTimelineCard
                key={group.itemCode}
                group={group}
                viewStartDate={viewStartDate}
                viewEndDate={viewEndDate}
                zoomLevel={zoomLevel}
                cellWidth={cellWidth}
                config={config}
                onScheduleClick={handleScheduleClick}
              />
            ))}
          </div>
        ) : (
          <div className="flex flex-1 items-center justify-center">
            <div className="text-center text-muted-foreground">
              {hasLinkedFilter && !hasReceivedSelection ? (
                <>
                  <Package className="mx-auto mb-2 h-8 w-8 opacity-30 sm:mb-3 sm:h-10 sm:w-10" />
                  <p className="text-xs font-medium sm:text-sm">
                    {linkedFilter?.emptyMessage || "좌측 목록에서 품목을 선택하세요"}
                  </p>
                  <p className="mt-1.5 max-w-[220px] text-[10px] sm:mt-2 sm:text-xs">
                    선택한 항목에 대한 생산계획 타임라인이 여기에 표시됩니다
                  </p>
                </>
              ) : hasLinkedFilter && hasReceivedSelection ? (
                <>
                  <Calendar className="mx-auto mb-2 h-8 w-8 opacity-30 sm:mb-3 sm:h-10 sm:w-10" />
                  <p className="text-xs font-medium sm:text-sm">
                    선택한 항목에 대한 스케줄이 없습니다
                  </p>
                  <p className="mt-1.5 max-w-[260px] text-[10px] sm:mt-2 sm:text-xs">
                    위 "자동 스케줄 생성" 버튼으로 생산계획을 생성하세요
                  </p>
                </>
              ) : (
                <>
                  <Calendar className="mx-auto mb-2 h-8 w-8 opacity-50 sm:mb-3 sm:h-10 sm:w-10" />
                  <p className="text-xs font-medium sm:text-sm">스케줄 데이터가 없습니다</p>
                </>
              )}
            </div>
          </div>
        )}

        {/* 범용 액션 미리보기 다이얼로그 */}
        {actionDialog && (
          <SchedulePreviewDialog
            open={true}
            onOpenChange={(open) => { if (!open) setActionDialog(null); }}
            isLoading={actionDialog.isLoading}
            summary={actionDialog.summary}
            previews={actionDialog.previews}
            deletedSchedules={actionDialog.deletedSchedules}
            keptSchedules={actionDialog.keptSchedules}
            onConfirm={handleActionApply}
            isApplying={actionDialog.isApplying}
            title={actionDialog.action.dialogTitle}
            description={actionDialog.action.dialogDescription}
          />
        )}
      </div>
    );
  }

  // ────────── 메인 렌더링 (리소스 기반) ──────────
  return (
    <div
      ref={containerRef}
      className="flex w-full flex-col overflow-hidden rounded-lg border bg-background"
      style={{
        height: config.height || 500,
        maxHeight: config.maxHeight,
      }}
    >
      {/* 툴바 */}
      {showToolbar && (
        <div className="flex shrink-0 items-center justify-between border-b bg-muted/30 px-2 py-1.5 sm:px-3 sm:py-2">
          {/* 네비게이션 */}
          <div className="flex items-center gap-0.5 sm:gap-1">
            {config.showNavigation !== false && (
              <>
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={goToPrevious}
                  className="h-6 px-1.5 sm:h-7 sm:px-2"
                >
                  <ChevronLeft className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={goToToday}
                  className="h-6 px-1.5 text-xs sm:h-7 sm:px-2 sm:text-sm"
                >
                  오늘
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={goToNext}
                  className="h-6 px-1.5 sm:h-7 sm:px-2"
                >
                  <ChevronRight className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                </Button>
              </>
            )}

            {/* 날짜 범위 표시 */}
            <span className="ml-1 text-[10px] text-muted-foreground sm:ml-2 sm:text-sm">
              {viewStartDate.getFullYear()}년 {viewStartDate.getMonth() + 1}월{" "}
              {viewStartDate.getDate()}일 ~ {viewEndDate.getMonth() + 1}월{" "}
              {viewEndDate.getDate()}일
            </span>
          </div>

          {/* 오른쪽 컨트롤 */}
          <div className="flex items-center gap-1 sm:gap-2">
            {/* 충돌 카운트 표시 */}
            {config.showConflicts !== false && conflictIds.size > 0 && (
              <span className="rounded-full bg-destructive/10 px-1.5 py-0.5 text-[9px] font-medium text-destructive sm:px-2 sm:text-[10px]">
                충돌 {conflictIds.size}건
              </span>
            )}

            {/* 줌 컨트롤 */}
            {config.showZoomControls !== false && (
              <div className="flex items-center gap-0.5 sm:gap-1">
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={handleZoomOut}
                  disabled={zoomLevel === "month"}
                  className="h-6 px-1.5 sm:h-7 sm:px-2"
                >
                  <ZoomOut className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                </Button>
                <span className="min-w-[20px] text-center text-[10px] text-muted-foreground sm:min-w-[24px] sm:text-xs">
                  {
                    zoomLevelOptions.find((o) => o.value === zoomLevel)
                      ?.label
                  }
                </span>
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={handleZoomIn}
                  disabled={zoomLevel === "day"}
                  className="h-6 px-1.5 sm:h-7 sm:px-2"
                >
                  <ZoomIn className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
                </Button>
              </div>
            )}

            {/* 추가 버튼 */}
            {config.showAddButton !== false && config.editable && (
              <Button
                variant="default"
                size="sm"
                onClick={handleAddClick}
                className="h-6 text-xs sm:h-7 sm:text-sm"
              >
                <Plus className="mr-0.5 h-3.5 w-3.5 sm:mr-1 sm:h-4 sm:w-4" />
                추가
              </Button>
            )}
          </div>
        </div>
      )}

      {/* 타임라인 본문 (스크롤 영역) */}
      <div ref={scrollContainerRef} className="min-h-0 flex-1 overflow-auto">
        <div className="min-w-max">
          {/* 헤더 */}
          <TimelineHeader
            startDate={viewStartDate}
            endDate={viewEndDate}
            zoomLevel={zoomLevel}
            cellWidth={cellWidth}
            headerHeight={headerHeight}
            resourceColumnWidth={resourceColumnWidth}
            showTodayLine={config.showTodayLine}
          />

          {/* 리소스 행들 - 30개 이상이면 가상 스크롤 */}
          {useVirtual ? (
            <div
              style={{
                height: `${virtualizer.getTotalSize()}px`,
                position: "relative",
              }}
            >
              {virtualizer.getVirtualItems().map((virtualRow) => {
                const resource = effectiveResources[virtualRow.index];
                return (
                  <div
                    key={resource.id}
                    style={{
                      position: "absolute",
                      top: virtualRow.start,
                      left: 0,
                      width: "100%",
                    }}
                  >
                    <ResourceRow
                      resource={resource}
                      schedules={
                        schedulesByResource.get(resource.id) || []
                      }
                      startDate={viewStartDate}
                      endDate={viewEndDate}
                      zoomLevel={zoomLevel}
                      rowHeight={rowHeight}
                      cellWidth={cellWidth}
                      resourceColumnWidth={resourceColumnWidth}
                      config={config}
                      conflictIds={conflictIds}
                      onScheduleClick={handleScheduleClick}
                      onCellClick={handleCellClick}
                      onDragComplete={handleDragComplete}
                      onResizeComplete={handleResizeComplete}
                    />
                  </div>
                );
              })}
            </div>
          ) : (
            <div>
              {effectiveResources.map((resource) => (
                <ResourceRow
                  key={resource.id}
                  resource={resource}
                  schedules={
                    schedulesByResource.get(resource.id) || []
                  }
                  startDate={viewStartDate}
                  endDate={viewEndDate}
                  zoomLevel={zoomLevel}
                  rowHeight={rowHeight}
                  cellWidth={cellWidth}
                  resourceColumnWidth={resourceColumnWidth}
                  config={config}
                  conflictIds={conflictIds}
                  onScheduleClick={handleScheduleClick}
                  onCellClick={handleCellClick}
                  onDragComplete={handleDragComplete}
                  onResizeComplete={handleResizeComplete}
                />
              ))}
            </div>
          )}
        </div>
      </div>

      {/* 범례 */}
      {showLegend && (
        <div className="shrink-0">
          <TimelineLegend config={config} />
        </div>
      )}
    </div>
  );
}
