"use client";

/**
 * InvRepeaterConfigPanel — 데이터 조회/선택 (v2-repeater) cp 톤 설정 패널
 *
 * 분류 체계 (Codex 검토 2026-04-28):
 *   RepKind  = "repeater"                                               (1)
 *   RepType  = "inline" | "modal" | "button"                            (button = reserved/disabled)
 *   RepFormat = "flat" | "grouped" | "pick-one" | "pick-many"
 *             | "source-detail" | "external"
 *
 * 흐름 (V2FieldConfigPanel 입력폼 패턴 — cp-panel-standard.md §2.1):
 *   CPCrumb (InvDataConfigPanel 가 그림)
 *   ① 데이터 소속 (always) → ② 유형별 설정 (CPFormatTrigger + FormatBody)
 *   → ③ 데이터 필터 (modal/source-detail 일 때) → ▾ 동작/편집 → ▾ 표시 → ▾ 컬럼 구성 → ▾ 고급
 *
 * cp 프리미티브만 (shadcn 직접 import 0건).
 *
 * Reference: notes/gbpark/2026-04-28-inv-repeater-redesign.md
 *            notes/gbpark/2026-04-28-cp-panel-standard.md
 */

import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import {
  Database,
  Link2,
  Plus,
  Trash2,
  ArrowRight,
  Calculator,
  Loader2,
  Wand2,
  Hash,
  Search,
  MousePointerClick,
  Rows3,
  Layers,
  PanelsTopLeft,
} from "lucide-react";
import {
  CPSection,
  CPRow,
  CPGroup,
  CPText,
  CPSelect,
  CPNumber,
  CPSwitch,
  CPSegment,
  CPIconBtn,
  CPFormatTrigger,
  type CPFormatItem,
} from "./_shared/cp";
import { tableTypeApi } from "@/lib/api/screen";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { getAvailableNumberingRules } from "@/lib/api/numberingRule";
import { NumberingRuleConfig } from "@/types/numbering-rule";
import {
  V2RepeaterConfig,
  RepeaterColumnConfig,
  DEFAULT_REPEATER_CONFIG,
} from "@/types/v2-repeater";

// ───────────────────────────────────────────────────────
// 분류 체계 (RepKind / RepType / RepFormat)
// ───────────────────────────────────────────────────────
type RepKind = "repeater";
type RepType = "inline" | "modal" | "button";
type RepFormat =
  | "flat"
  | "grouped"
  | "pick-one"
  | "pick-many"
  | "source-detail"
  | "external";

const TYPES: Array<{ id: RepType; name: string; desc: string; disabled?: boolean; icon: React.ReactNode }> = [
  { id: "inline", name: "직접 입력", desc: "행을 화면에서 직접 추가", icon: <Rows3 size={14} /> },
  { id: "modal", name: "모달 선택", desc: "엔티티 검색 후 추가", icon: <Search size={14} /> },
  { id: "button", name: "버튼 연결", desc: "외부 화면으로 연결 (예약)", icon: <MousePointerClick size={14} />, disabled: true },
];

const FORMATS_BY_TYPE: Record<RepType, CPFormatItem[]> = {
  inline: [
    { id: "flat", name: "평면", desc: "행을 일직선으로", icon: <Rows3 size={11} /> },
    { id: "grouped", name: "그룹화", desc: "컬럼별 그룹", icon: <Layers size={11} /> },
  ],
  modal: [
    { id: "pick-many", name: "다건 추가", desc: "여러 행 한번에", icon: <ListPlusIcon /> },
    { id: "pick-one", name: "단건 추가", desc: "한 번에 한 행만", icon: <PlusOneIcon /> },
    { id: "source-detail", name: "마스터→자식", desc: "선택 시 자식 자동 채움", icon: <Layers size={11} /> },
  ],
  button: [
    { id: "external", name: "외부 화면", desc: "다른 화면으로 이동", icon: <PanelsTopLeft size={11} /> },
  ],
};

// 작은 인라인 아이콘 (lucide 추가 import 줄이기)
function ListPlusIcon() {
  return (
    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M11 12H3" />
      <path d="M16 6H3" />
      <path d="M16 18H3" />
      <path d="M18 9v6" />
      <path d="M21 12h-6" />
    </svg>
  );
}
function PlusOneIcon() {
  return (
    <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="9" />
      <path d="M12 8v8" />
      <path d="M8 12h8" />
    </svg>
  );
}

// ───────────────────────────────────────────────────────
// resolveTriple / applyTriple — legacy ↔ taxonomy
// ───────────────────────────────────────────────────────
function resolveTriple(config: V2RepeaterConfig): { kind: RepKind; type: RepType; format: RepFormat } {
  const renderMode = (config?.render_mode || "inline") as RepType;
  const type: RepType = renderMode === "modal" ? "modal" : renderMode === "button" ? "button" : "inline";

  let format: RepFormat;
  if (type === "inline") {
    format = "flat"; // grouped 는 추후 grouped_by 키 도입 시 분기
  } else if (type === "modal") {
    if (config?.source_detail_config) format = "source-detail";
    else if (config?.modal && (config.modal as any).allow_multiple === false) format = "pick-one";
    else format = "pick-many";
  } else {
    format = "external";
  }
  return { kind: "repeater", type, format };
}

function applyTriple(
  prev: V2RepeaterConfig,
  type: RepType,
  format: RepFormat,
): V2RepeaterConfig {
  const next: V2RepeaterConfig = { ...prev };
  next.render_mode = type === "button" ? ("button" as any) : (type as "inline" | "modal");

  if (type === "modal") {
    if (format === "source-detail") {
      next.source_detail_config = next.source_detail_config || {
        table_name: "",
        foreign_key: "",
        parent_key: "",
      };
    } else {
      // pick-one / pick-many 는 source_detail_config 비활성 (즉시 삭제 X — 보존)
      // 사용자 명시 OFF 시에만 삭제
    }
    next.modal = {
      ...(next.modal || {}),
      ...(format === "pick-one" ? ({ allow_multiple: false } as any) : ({ allow_multiple: true } as any)),
    };
  } else if (type === "inline") {
    // inline 일 때 modal/data_source 키는 보존만 (DB 호환)
  }

  return next;
}

// ───────────────────────────────────────────────────────
// 보조 타입
// ───────────────────────────────────────────────────────
interface InvRepeaterConfigPanelProps {
  config: V2RepeaterConfig;
  onChange: (config: V2RepeaterConfig) => void;
  currentTableName?: string;
  screenTableName?: string;
  tableColumns?: any[];
  menuObjid?: number | string;
}

interface ColumnOption {
  columnName: string;
  displayName: string;
  input_type?: string;
  detailSettings?: {
    codeGroup?: string;
    referenceTable?: string;
    referenceColumn?: string;
    displayColumn?: string;
    format?: string;
  };
}

interface EntityColumnOption {
  columnName: string;
  displayName: string;
  referenceTable?: string;
  referenceColumn?: string;
  displayColumn?: string;
}

interface CalcRule {
  id: string;
  target_column: string;
  formula: string;
  label?: string;
}

interface TableRelation {
  tableName: string;
  tableLabel: string;
  foreign_key_column: string;
  referenceColumn: string;
}

const AUTO_FILL_OPTIONS: Array<{ value: string; label: string }> = [
  { value: "none", label: "없음" },
  { value: "currentDate", label: "현재 날짜" },
  { value: "currentDateTime", label: "현재 날짜+시간" },
  { value: "sequence", label: "순번 (1, 2, 3...)" },
  { value: "numbering", label: "채번 규칙" },
  { value: "fromMainForm", label: "메인 폼에서 복사" },
  { value: "fixed", label: "고정값" },
  { value: "parentSequence", label: "부모채번+순번" },
];

// ───────────────────────────────────────────────────────
// Hint / DimText / InlineLoader (cp 톤)
// ───────────────────────────────────────────────────────
function Hint({ children, tone = "default" }: { children: React.ReactNode; tone?: "default" | "warn" | "ok" }) {
  const color =
    tone === "warn" ? "var(--v5-amber, #d97706)" : tone === "ok" ? "rgb(16, 185, 129)" : "var(--cp-text-muted)";
  return (
    <div style={{ fontSize: 10.5, color, marginTop: 4, lineHeight: 1.5 }}>
      {children}
    </div>
  );
}

function DimText({ value, mono, muted }: { value: React.ReactNode; mono?: boolean; muted?: boolean }) {
  return (
    <div
      style={{
        height: 28,
        padding: "0 8px",
        display: "flex",
        alignItems: "center",
        fontSize: 12,
        fontFamily: mono ? "var(--v5-font-mono)" : "var(--v5-font-sans)",
        background: "var(--cp-bg-subtle)",
        border: "1px solid var(--cp-border)",
        borderRadius: 6,
        color: muted ? "var(--cp-text-muted)" : "var(--cp-text)",
      }}
    >
      {value}
    </div>
  );
}

function InlineLoader({ text }: { text: string }) {
  return (
    <span style={{ fontSize: 11, color: "var(--cp-text-muted)", display: "inline-flex", alignItems: "center", gap: 5 }}>
      <Loader2 size={11} className="animate-spin" /> {text}
    </span>
  );
}

// 작은 칩 (선택된 컬럼 표시 등)
function CpChip({
  active,
  onClick,
  children,
  tone = "default",
}: {
  active?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
  tone?: "default" | "primary";
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      style={{
        display: "inline-flex",
        alignItems: "center",
        gap: 4,
        padding: "3px 8px",
        background: active
          ? "rgba(var(--v5-primary-rgb), 0.14)"
          : "var(--cp-bg-subtle)",
        border: `1px solid ${
          active
            ? "rgba(var(--v5-primary-rgb), 0.5)"
            : "var(--cp-border-subtle)"
        }`,
        borderRadius: 4,
        fontSize: 10.5,
        fontWeight: active ? 600 : 500,
        color: active ? "var(--v5-primary, #6c5ce7)" : tone === "primary" ? "var(--cp-text)" : "var(--cp-text-sec)",
        cursor: "pointer",
        fontFamily: "var(--v5-font-sans)",
      }}
    >
      {children}
    </button>
  );
}

// ───────────────────────────────────────────────────────
// Main Component
// ───────────────────────────────────────────────────────
export const InvRepeaterConfigPanel: React.FC<InvRepeaterConfigPanelProps> = ({
  config: propConfig,
  onChange,
  currentTableName: propCurrentTableName,
  screenTableName,
  menuObjid,
}) => {
  const currentTableName = screenTableName || propCurrentTableName;

  // config 안전 초기화
  const config: V2RepeaterConfig = useMemo(() => {
    return {
      ...DEFAULT_REPEATER_CONFIG,
      ...propConfig,
      render_mode: propConfig?.render_mode || DEFAULT_REPEATER_CONFIG.render_mode,
      data_source: { ...DEFAULT_REPEATER_CONFIG.data_source, ...propConfig?.data_source },
      columns: propConfig?.columns || [],
      modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig?.modal } as V2RepeaterConfig["modal"],
      features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig?.features } as V2RepeaterConfig["features"],
    } as V2RepeaterConfig;
  }, [propConfig]);

  // ── State ────────────────────────────────────────────
  const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
  const [loadingTables, setLoadingTables] = useState(false);
  const [relatedTables, setRelatedTables] = useState<TableRelation[]>([]);
  const [loadingRelations, setLoadingRelations] = useState(false);

  const [currentTableColumns, setCurrentTableColumns] = useState<ColumnOption[]>([]);
  const [entityColumns, setEntityColumns] = useState<EntityColumnOption[]>([]);
  const [sourceTableColumns, setSourceTableColumns] = useState<ColumnOption[]>([]);
  const [loadingColumns, setLoadingColumns] = useState(false);
  const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);

  const [entityJoinData, setEntityJoinData] = useState<{
    joinTables: Array<{
      tableName: string;
      currentDisplayColumn: string;
      joinConfig?: { sourceColumn?: string; source_column?: string };
      availableColumns: Array<{
        columnName: string;
        columnLabel: string;
        dataType: string;
        input_type?: string;
      }>;
    }>;
    availableColumns: Array<any>;
  }>({ joinTables: [], availableColumns: [] });
  const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);

  const [calcRules, setCalcRules] = useState<CalcRule[]>(config.calculation_rules || []);
  const [expandedColumn, setExpandedColumn] = useState<string | null>(null);

  const [parentMenus, setParentMenus] = useState<any[]>([]);
  const [loadingMenus, setLoadingMenus] = useState(false);
  const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
  const [loadingNumberingRules, setLoadingNumberingRules] = useState(false);
  const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
    const existing = config.columns.find(
      (c) => c.auto_fill?.type === "numbering" && c.auto_fill.selected_menu_objid,
    );
    return existing?.auto_fill?.selected_menu_objid || (menuObjid ? Number(menuObjid) : undefined);
  });

  // ── Derived ──────────────────────────────────────────
  const triple = useMemo(() => resolveTriple(config), [config]);
  const visibleFormats = FORMATS_BY_TYPE[triple.type] || [];
  const showFormatTrigger = visibleFormats.length > 1;
  const isInlineMode = triple.type === "inline";
  const isModalMode = triple.type === "modal";
  const isSourceDetail = isModalMode && triple.format === "source-detail";

  const targetTableForColumns = config.use_custom_table && config.main_table_name
    ? config.main_table_name
    : currentTableName;

  const entityJoinTargetTable = config.use_custom_table && config.main_table_name
    ? config.main_table_name
    : currentTableName;

  const inputableColumns = useMemo(() => {
    const fkColumn = config.data_source?.foreign_key;
    return currentTableColumns.filter(
      (col) => col.columnName !== fkColumn && col.input_type !== "entity",
    );
  }, [currentTableColumns, config.data_source?.foreign_key]);

  // ── Helpers ──────────────────────────────────────────
  const updateConfig = useCallback(
    (updates: Partial<V2RepeaterConfig>) => {
      onChange({ ...config, ...updates });
    },
    [config, onChange],
  );

  const updateFeatures = useCallback(
    (field: string, value: boolean) => {
      updateConfig({ features: { ...config.features, [field]: value } as any });
    },
    [config.features, updateConfig],
  );

  const updateModal = useCallback(
    (field: string, value: any) => {
      updateConfig({ modal: { ...config.modal, [field]: value } as V2RepeaterConfig["modal"] });
    },
    [config.modal, updateConfig],
  );

  const updateStyle = useCallback(
    (field: string, value: any) => {
      updateConfig({ style: { ...(config.style || {}), [field]: value } });
    },
    [config.style, updateConfig],
  );

  const handleTypeChange = useCallback(
    (newType: RepType) => {
      const def = FORMATS_BY_TYPE[newType][0]?.id as RepFormat;
      const next = applyTriple(config, newType, def);
      onChange(next);
    },
    [config, onChange],
  );

  const handleFormatChange = useCallback(
    (newFormat: string) => {
      const next = applyTriple(config, triple.type, newFormat as RepFormat);
      onChange(next);
    },
    [config, triple.type, onChange],
  );

  const handleSaveTableSelect = useCallback(
    (tableName: string) => {
      if (!tableName || tableName === currentTableName) {
        updateConfig({
          use_custom_table: false,
          main_table_name: undefined,
          foreign_key_column: undefined,
          foreign_key_source_column: undefined,
        });
        return;
      }
      const relation = relatedTables.find((r) => r.tableName === tableName);
      if (relation) {
        updateConfig({
          use_custom_table: true,
          main_table_name: tableName,
          foreign_key_column: relation.foreign_key_column,
          foreign_key_source_column: relation.referenceColumn,
        });
      } else {
        updateConfig({
          use_custom_table: true,
          main_table_name: tableName,
          foreign_key_column: undefined,
          foreign_key_source_column: "id",
        });
      }
    },
    [currentTableName, relatedTables, updateConfig],
  );

  const handleEntityColumnSelect = (columnName: string) => {
    const sel = entityColumns.find((c) => c.columnName === columnName);
    if (!sel) return;
    const displayColInfo = sourceTableColumns.find((c) => c.columnName === sel.displayColumn);
    const displayLabel = displayColInfo?.displayName || sel.displayColumn || "";
    updateConfig({
      data_source: {
        ...config.data_source,
        source_table: sel.referenceTable || "",
        foreign_key: sel.columnName,
        reference_key: sel.referenceColumn || "id",
        display_column: sel.displayColumn,
      },
      modal: {
        ...config.modal,
        search_fields: sel.displayColumn ? [sel.displayColumn] : [],
        source_display_columns: sel.displayColumn
          ? [{ key: sel.displayColumn, label: displayLabel }]
          : [],
      } as V2RepeaterConfig["modal"],
    });
  };

  const toggleInputColumn = (col: ColumnOption) => {
    const existingIdx = config.columns.findIndex(
      (c) => c.key === col.columnName && !c.is_join_column && !c.is_source_display,
    );
    if (existingIdx >= 0) {
      updateConfig({ columns: config.columns.filter((_, i) => i !== existingIdx) });
    } else {
      const next: RepeaterColumnConfig = {
        key: col.columnName,
        title: col.displayName,
        width: "auto",
        visible: true,
        editable: true,
        input_type: col.input_type || "text",
        detail_settings: col.detailSettings
          ? {
              code_group: col.detailSettings.codeGroup,
              reference_table: col.detailSettings.referenceTable,
              reference_column: col.detailSettings.referenceColumn,
              display_column: col.detailSettings.displayColumn,
              format: col.detailSettings.format,
            }
          : undefined,
      };
      updateConfig({ columns: [...config.columns, next] });
    }
  };

  const toggleSourceDisplayColumn = (col: ColumnOption) => {
    const exists = config.columns.some((c) => c.key === col.columnName && c.is_source_display);
    if (exists) {
      updateConfig({ columns: config.columns.filter((c) => c.key !== col.columnName) });
    } else {
      updateConfig({
        columns: [
          ...config.columns,
          {
            key: col.columnName,
            title: col.displayName,
            width: "auto",
            visible: true,
            editable: false,
            is_source_display: true,
          },
        ],
      });
    }
  };

  const isColumnAdded = (name: string) =>
    config.columns.some((c) => c.key === name && !c.is_source_display && !c.is_join_column);
  const isSourceColumnSelected = (name: string) =>
    config.columns.some((c) => c.key === name && c.is_source_display);

  const updateColumnProp = (key: string, field: keyof RepeaterColumnConfig, value: any) => {
    updateConfig({
      columns: config.columns.map((c) => (c.key === key ? { ...c, [field]: value } : c)),
    });
  };

  const removeColumn = (key: string) => {
    const col = config.columns.find((c) => c.key === key);
    if (!col) return;
    if (col.is_join_column) {
      const newColumns = config.columns.filter((c) => c.key !== key);
      const newJoins = config.entity_joins
        ?.map((j) => ({ ...j, columns: j.columns.filter((c) => c.display_field !== key) }))
        .filter((j) => j.columns.length > 0);
      updateConfig({ columns: newColumns, entity_joins: newJoins });
    } else {
      updateConfig({ columns: config.columns.filter((c) => c.key !== key) });
    }
  };

  const moveColumn = (fromIdx: number, toIdx: number) => {
    if (fromIdx === toIdx) return;
    const next = [...config.columns];
    const [moved] = next.splice(fromIdx, 1);
    next.splice(toIdx, 0, moved);
    updateConfig({ columns: next });
  };

  const toggleEntityJoinColumn = (
    joinTableName: string,
    sourceColumn: string,
    refColumnName: string,
    refColumnLabel: string,
    displayField: string,
    columnType?: string,
  ) => {
    const currentJoins = config.entity_joins || [];
    const idx = currentJoins.findIndex(
      (j) => j.source_column === sourceColumn && j.reference_table === joinTableName,
    );
    let newJoins = [...currentJoins];
    let newColumns = [...config.columns];

    if (idx >= 0) {
      const cur = currentJoins[idx];
      const colIdx = cur.columns.findIndex((c) => c.reference_field === refColumnName);
      if (colIdx >= 0) {
        const updated = cur.columns.filter((_, i) => i !== colIdx);
        if (updated.length === 0) newJoins = newJoins.filter((_, i) => i !== idx);
        else newJoins[idx] = { ...cur, columns: updated };
        newColumns = newColumns.filter((c) => !(c.key === displayField && c.is_join_column));
      } else {
        newJoins[idx] = {
          ...cur,
          columns: [...cur.columns, { reference_field: refColumnName, display_field: displayField }],
        };
        newColumns.push({
          key: displayField,
          title: refColumnLabel,
          width: "auto",
          visible: true,
          editable: false,
          is_join_column: true,
          input_type: columnType || "text",
        });
      }
    } else {
      newJoins.push({
        source_column: sourceColumn,
        reference_table: joinTableName,
        columns: [{ reference_field: refColumnName, display_field: displayField }],
      });
      newColumns.push({
        key: displayField,
        title: refColumnLabel,
        width: "auto",
        visible: true,
        editable: false,
        is_join_column: true,
        input_type: columnType || "text",
      });
    }
    updateConfig({ entity_joins: newJoins, columns: newColumns });
  };

  const isEntityJoinColumnActive = (joinTableName: string, sourceColumn: string, refColumnName: string) =>
    (config.entity_joins || []).some(
      (j) =>
        j.source_column === sourceColumn &&
        j.reference_table === joinTableName &&
        j.columns.some((c) => c.reference_field === refColumnName),
    );

  // 계산 규칙
  const syncCalcRules = (rules: CalcRule[]) => {
    setCalcRules(rules);
    updateConfig({ calculation_rules: rules });
  };
  const addCalcRule = () =>
    syncCalcRules([...calcRules, { id: `calc_${Date.now()}`, target_column: "", formula: "" }]);
  const removeCalcRule = (id: string) => syncCalcRules(calcRules.filter((r) => r.id !== id));
  const updateCalcRule = (id: string, field: keyof CalcRule, value: string) =>
    syncCalcRules(calcRules.map((r) => (r.id === id ? { ...r, [field]: value } : r)));
  const insertColToFormula = (id: string, key: string) => {
    const r = calcRules.find((x) => x.id === id);
    if (!r) return;
    const next = r.formula ? `${r.formula} ${key}` : key;
    updateCalcRule(id, "formula", next);
  };
  const formulaToKorean = (formula: string): string => {
    if (!formula) return "";
    let out = formula;
    const sorted = [...config.columns].sort((a, b) => b.key.length - a.key.length);
    for (const c of sorted) {
      if (c.title && c.key) out = out.replace(new RegExp(`\\b${c.key}\\b`, "g"), c.title);
    }
    return out;
  };

  // ── Effects ──────────────────────────────────────────
  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoadingTables(true);
      try {
        const r = await tableManagementApi.getTableList();
        if (cancelled) return;
        if (r.success && r.data) {
          setAllTables(
            r.data.map((t: any) => ({
              tableName: t.tableName || t.table_name,
              displayName: t.displayName || t.table_label || t.tableName || t.table_name,
            })),
          );
        }
      } catch (e) {
        console.error("[InvRepeater] 테이블 목록 로드 실패:", e);
      } finally {
        if (!cancelled) setLoadingTables(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const base = currentTableName || config.main_table_name;
      if (!base) {
        setRelatedTables([]);
        return;
      }
      setLoadingRelations(true);
      try {
        const { apiClient } = await import("@/lib/api/client");
        const all: TableRelation[] = [];
        if (currentTableName) {
          const r = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
          if (r.data.success && r.data.data) {
            r.data.data.forEach((rel: any) => {
              all.push({
                tableName: rel.tableName || rel.table_name,
                tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
                foreign_key_column: rel.columnName || rel.column_name,
                referenceColumn: rel.referenceColumn || rel.reference_column || "id",
              });
            });
          }
        }
        if (config.main_table_name && config.main_table_name !== currentTableName) {
          const r2 = await apiClient.get(`/table-management/columns/${config.main_table_name}/referenced-by`);
          if (r2.data.success && r2.data.data) {
            r2.data.data.forEach((rel: any) => {
              const exists = all.some((x) => x.tableName === (rel.tableName || rel.table_name));
              if (!exists)
                all.push({
                  tableName: rel.tableName || rel.table_name,
                  tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
                  foreign_key_column: rel.columnName || rel.column_name,
                  referenceColumn: rel.referenceColumn || rel.reference_column || "id",
                });
            });
          }
        }
        if (!cancelled) setRelatedTables(all);
      } catch (e) {
        console.error("[InvRepeater] 연관 테이블 로드 실패:", e);
        if (!cancelled) setRelatedTables([]);
      } finally {
        if (!cancelled) setLoadingRelations(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [currentTableName, config.main_table_name]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!targetTableForColumns) {
        setCurrentTableColumns([]);
        setEntityColumns([]);
        return;
      }
      setLoadingColumns(true);
      try {
        const data = await tableTypeApi.getColumns(targetTableForColumns);
        if (cancelled) return;
        const cols: ColumnOption[] = [];
        const ents: EntityColumnOption[] = [];
        for (const c of data) {
          let detail: any = null;
          if ((c as any).detailSettings) {
            try {
              detail =
                typeof (c as any).detailSettings === "string"
                  ? JSON.parse((c as any).detailSettings)
                  : (c as any).detailSettings;
            } catch {}
          }
          const co: ColumnOption = {
            columnName: (c as any).columnName || (c as any).column_name,
            displayName:
              (c as any).displayName || (c as any).columnLabel || (c as any).columnName || (c as any).column_name,
            input_type: (c as any).input_type,
            detailSettings: detail
              ? {
                  codeGroup: detail.codeGroup,
                  referenceTable: detail.reference_table,
                  referenceColumn: detail.referenceColumn,
                  displayColumn: detail.displayColumn,
                  format: detail.format,
                }
              : undefined,
          };
          cols.push(co);
          if (co.input_type === "entity") {
            const refTable = detail?.reference_table || (c as any).reference_table;
            if (refTable) {
              ents.push({
                columnName: co.columnName,
                displayName: co.displayName,
                referenceTable: refTable,
                referenceColumn: detail?.referenceColumn || (c as any).referenceColumn || "id",
                displayColumn: detail?.displayColumn || (c as any).displayColumn,
              });
            }
          }
        }
        setCurrentTableColumns(cols);
        setEntityColumns(ents);
      } catch (e) {
        console.error("[InvRepeater] 저장 테이블 컬럼 로드 실패:", e);
        if (!cancelled) {
          setCurrentTableColumns([]);
          setEntityColumns([]);
        }
      } finally {
        if (!cancelled) setLoadingColumns(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [targetTableForColumns]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      const src = config.data_source?.source_table;
      if (!src || !isModalMode) {
        setSourceTableColumns([]);
        return;
      }
      setLoadingSourceColumns(true);
      try {
        const data = await tableTypeApi.getColumns(src);
        if (cancelled) return;
        setSourceTableColumns(
          data.map((c: any) => ({
            columnName: c.columnName || c.column_name,
            displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
            input_type: c.input_type,
          })),
        );
      } catch (e) {
        console.error("[InvRepeater] 소스 테이블 컬럼 로드 실패:", e);
        if (!cancelled) setSourceTableColumns([]);
      } finally {
        if (!cancelled) setLoadingSourceColumns(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [config.data_source?.source_table, isModalMode]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!entityJoinTargetTable) return;
      setLoadingEntityJoins(true);
      try {
        const r = await entityJoinApi.getEntityJoinColumns(entityJoinTargetTable);
        if (cancelled) return;
        setEntityJoinData({ joinTables: (r as any).joinTables || [], availableColumns: (r as any).availableColumns || [] });
      } catch (e) {
        console.error("[InvRepeater] Entity 조인 로드 실패:", e);
        if (!cancelled) setEntityJoinData({ joinTables: [], availableColumns: [] });
      } finally {
        if (!cancelled) setLoadingEntityJoins(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [entityJoinTargetTable]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      setLoadingMenus(true);
      try {
        const { apiClient } = await import("@/lib/api/client");
        const r = await apiClient.get("/admin/menus");
        if (cancelled) return;
        if (r.data.success && r.data.data) {
          setParentMenus(r.data.data.filter((m: any) => m.menu_type === "1" && m.lev === 2));
        }
      } catch (e) {
        console.error("[InvRepeater] 메뉴 로드 실패:", e);
      } finally {
        if (!cancelled) setLoadingMenus(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (!selectedMenuObjid) {
        setNumberingRules([]);
        return;
      }
      setLoadingNumberingRules(true);
      try {
        const r = await getAvailableNumberingRules(selectedMenuObjid);
        if (cancelled) return;
        if ((r as any)?.success && (r as any).data) setNumberingRules((r as any).data);
      } catch (e) {
        console.error("[InvRepeater] 채번 규칙 로드 실패:", e);
        if (!cancelled) setNumberingRules([]);
      } finally {
        if (!cancelled) setLoadingNumberingRules(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [selectedMenuObjid]);

  // ── Render ───────────────────────────────────────────
  return (
    <div style={{ fontFamily: "var(--v5-font-sans)", color: "var(--cp-text)", padding: "0 12px" }}>
      {/* ── ① 데이터 소속 ─────────────────────────── */}
      <CPSection title="① 데이터 소속" desc="저장 테이블 + FK 매핑">
        <CPRow label="저장 테이블">
          <CPSelect
            value={
              config.use_custom_table && config.main_table_name
                ? config.main_table_name
                : currentTableName || ""
            }
            onChange={(v) => v && handleSaveTableSelect(v)}
            disabled={loadingTables || loadingRelations}
          >
            <option value="">테이블 선택</option>
            {currentTableName && (
              <option value={currentTableName}>{currentTableName} (화면 메인)</option>
            )}
            {relatedTables.map((rel) => (
              <option key={rel.tableName} value={rel.tableName}>
                {rel.tableLabel} (연관 · FK 자동)
              </option>
            ))}
            {allTables
              .filter(
                (t) =>
                  t.tableName !== currentTableName &&
                  !relatedTables.some((r) => r.tableName === t.tableName),
              )
              .map((t) => (
                <option key={t.tableName} value={t.tableName}>
                  {t.displayName}
                </option>
              ))}
          </CPSelect>
        </CPRow>

        {config.use_custom_table &&
          config.main_table_name &&
          currentTableName &&
          !relatedTables.some((r) => r.tableName === config.main_table_name) && (
            <>
              <CPRow label="FK 컬럼" help="저장 테이블의 FK 컬럼명">
                <CPText
                  value={config.foreign_key_column || ""}
                  onChange={(v) => updateConfig({ foreign_key_column: v })}
                  placeholder="예: master_id"
                />
              </CPRow>
              <CPRow label="PK 컬럼" help="화면 테이블의 PK 컬럼명">
                <CPText
                  value={config.foreign_key_source_column || "id"}
                  onChange={(v) => updateConfig({ foreign_key_source_column: v })}
                  placeholder="id"
                />
              </CPRow>
            </>
          )}

        {currentTableName && (
          <Hint>
            화면 메인:{" "}
            <span style={{ fontWeight: 700, color: "var(--cp-text)" }}>{currentTableName}</span>
            {" · "}컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개
          </Hint>
        )}
        {config.use_custom_table && config.main_table_name && !currentTableName && (
          <Hint tone="ok">독립 저장 모드 · 화면 테이블 없이 직접 저장</Hint>
        )}
      </CPSection>

      {/* ── ② 유형별 설정 ─────────────────────────── */}
      <CPSection title="② 유형별 설정" desc="입력 방식과 변형">
        <CPRow label="입력 방식">
          <CPSegment
            value={triple.type}
            onChange={(v) => handleTypeChange(v as RepType)}
            options={TYPES.map((t) => ({
              value: t.id,
              label: t.name,
              disabled: t.disabled,
            }))}
          />
        </CPRow>

        {showFormatTrigger && (
          <CPFormatTrigger
            formats={visibleFormats}
            value={triple.format}
            onChange={handleFormatChange}
          />
        )}

        {/* format 별 본문 */}
        {isInlineMode && (
          <>
            {triple.format === "grouped" && (
              <Hint tone="warn">그룹화 모드는 컬럼 그룹 설정 UI 가 아직 없습니다 (백엔드 키만 보존).</Hint>
            )}
          </>
        )}

        {isModalMode && (
          <>
            <CPRow label="검색 엔티티" help="모달에서 검색할 엔티티 (FK 컬럼)">
              {loadingColumns ? (
                <InlineLoader text="컬럼 로딩..." />
              ) : (
                <CPSelect
                  value={config.data_source?.foreign_key || ""}
                  onChange={handleEntityColumnSelect}
                  disabled={!targetTableForColumns}
                >
                  <option value="">
                    {!targetTableForColumns
                      ? "저장 테이블 먼저 선택"
                      : entityColumns.length === 0
                        ? "엔티티 타입 컬럼 없음"
                        : "엔티티 컬럼 선택"}
                  </option>
                  {entityColumns.map((col) => (
                    <option key={col.columnName} value={col.columnName}>
                      {col.displayName} → {col.referenceTable}
                    </option>
                  ))}
                </CPSelect>
              )}
            </CPRow>

            {config.data_source?.source_table && (
              <Hint>
                선택된 엔티티:{" "}
                <span style={{ fontWeight: 700, color: "var(--cp-text)" }}>
                  {config.data_source.source_table}
                </span>
                {" · "}
                {config.data_source.foreign_key} 컬럼에 FK 저장
              </Hint>
            )}

            <CPRow label="모달 제목">
              <CPText
                value={config.modal?.title || ""}
                onChange={(v) => updateModal("title", v)}
                placeholder="항목 검색"
              />
            </CPRow>
            <CPRow label="검색 버튼">
              <CPText
                value={config.modal?.button_text || ""}
                onChange={(v) => updateModal("button_text", v)}
                placeholder="검색"
              />
            </CPRow>

            {isSourceDetail && (
              <SourceDetailEditor
                value={config.source_detail_config}
                allTables={allTables}
                onChange={(next) => updateConfig({ source_detail_config: next })}
              />
            )}
          </>
        )}

        {triple.type === "button" && (
          <Hint tone="warn">버튼 연결 모드는 예약(reserved) — 런타임 미구현</Hint>
        )}
      </CPSection>

      {/* ── ③ 데이터 필터 (modal/source-detail 일 때) ─── */}
      {isModalMode && config.data_source?.source_table && (
        <CPSection title="③ 데이터 필터" desc={`${config.data_source.source_table} 검색 시 WHERE 조건`}>
          <CPRow label="필터 컬럼">
            <CPText
              value={config.data_source.filter?.column || ""}
              onChange={(v) =>
                updateConfig({
                  data_source: {
                    ...config.data_source,
                    filter: { ...(config.data_source.filter || { column: "", value: "" }), column: v },
                  },
                })
              }
              placeholder="status"
            />
          </CPRow>
          <CPRow label="필터 값">
            <CPText
              value={config.data_source.filter?.value || ""}
              onChange={(v) =>
                updateConfig({
                  data_source: {
                    ...config.data_source,
                    filter: { ...(config.data_source.filter || { column: "", value: "" }), value: v },
                  },
                })
              }
              placeholder="ACTIVE"
            />
          </CPRow>
        </CPSection>
      )}

      {/* ── ▾ 동작 / 편집 ─────────────────────────── */}
      <CPGroup title="동작 / 편집" defaultOpen>
        <FeatureChipGrid
          items={[
            {
              key: "show_add_button",
              label: "추가",
              default: true,
              desc: "리피터 우상단에 [+ 추가] 버튼이 표시됩니다.\n클릭 시 빈 행이 한 줄 추가되고 사용자가 컬럼별로 직접 입력해요.\n모달 모드에서는 검색 모달이 함께 열립니다.",
            },
            {
              key: "show_delete_button",
              label: "삭제",
              default: true,
              desc: "선택된 행을 제거합니다 (체크박스 또는 행 우측 🗑).\n저장 시 DB 에서도 함께 삭제되며, FK 로 연결된 자식 행이 있으면 cascade 정책에 따라 함께 지워질 수 있어요.",
            },
            {
              key: "inline_edit",
              label: "인라인",
              desc: "셀을 클릭하는 즉시 편집 모드로 전환됩니다.\n별도 [수정] 버튼이 필요 없어 입력 속도가 빠르고, 빠른 데이터 입력에 유리해요.\nOFF 시: 더블클릭이나 별도 폼 모달이 필요합니다.",
            },
            {
              key: "multi_select",
              label: "다중 선택",
              default: true,
              desc: "Shift/Ctrl + 클릭으로 여러 행을 동시에 선택할 수 있어요.\n일괄 삭제, 일괄 복제, 일괄 상태 변경 등 bulk action 에 필요합니다.\nOFF 시 한 번에 한 행만 선택 가능.",
            },
            {
              key: "drag_sort",
              label: "드래그",
              desc: "행 좌측의 ⋮⋮ 핸들을 잡아 순서를 자유롭게 변경할 수 있어요.\n작업 순서가 의미 있는 데이터 (공정 단계, 결재 라인, 우선순위 등)에 권장.\nDB 에는 sort_order 컬럼이 자동 갱신됩니다.",
            },
            {
              key: "duplicate_row",
              label: "복제",
              isNew: true,
              desc: "행 우측 메뉴에서 [복제] 선택 시 동일 데이터로 새 행이 바로 아래 추가됩니다.\n비슷한 항목 여러 개를 빠르게 입력할 때 유용해요 (예: 같은 품목 다른 수량/일자).",
            },
            {
              key: "inline_validation",
              label: "실시간 검증",
              isNew: true,
              desc: "필수/패턴/범위 위반 시 셀 테두리가 빨간색으로 표시되고 호버하면 이유가 나타납니다.\n저장 [전에] 오류를 발견할 수 있어 사용자 경험이 개선돼요.\n검증 규칙은 컬럼별 설정에서 정의합니다.",
            },
            {
              key: "keyboard_nav",
              label: "키보드",
              desc: "Enter = 다음 행으로 이동\nTab = 다음 셀, Shift+Tab = 이전 셀\n방향키 = 셀 단위 이동\n마우스 없이 빠른 대량 입력이 가능합니다 (Excel 스타일).",
            },
          ]}
          source={config.features as any}
          onToggle={updateFeatures}
        />
      </CPGroup>

      {/* ── ▾ 표시 ─────────────────────────────── */}
      <CPGroup title="표시" defaultOpen={false}>
        <FeatureChipGrid
          items={[
            {
              key: "show_row_number",
              label: "행 번호",
              desc: "행 맨 앞에 1, 2, 3... 순번이 자동 표시됩니다.\n사용자에게 행 위치를 시각적으로 알려줄 때 (인쇄, 보고서) 사용.\nDB 에는 저장되지 않는 표시 전용 번호.",
            },
            {
              key: "selectable",
              label: "체크박스",
              desc: "행 맨 앞에 체크박스 컬럼이 추가됩니다.\n다중 선택을 시각적으로 보여주며, 헤더 체크로 전체 선택 가능.\n[다중 선택] 기능과 함께 켜는 게 일반적이에요.",
            },
            {
              key: "footer_summary",
              label: "합계 행",
              desc: "숫자 컬럼 (수량, 금액, 시간 등) 의 합계가 표 하단에 자동 표시됩니다.\n컬럼별로 SUM / AVG / MIN / MAX / COUNT 중 선택 가능.\n총합/소계가 중요한 매출/원가 화면에 적합.",
            },
            {
              key: "sticky_header",
              label: "헤더 고정",
              desc: "스크롤해도 컬럼 헤더가 상단에 고정됩니다.\n행이 많은 표 (50행+) 에서 컬럼명이 안 보여 헷갈리는 문제를 해결.\n고정 행의 z-index 충돌 시 [최대 높이] 도 함께 설정하세요.",
            },
            {
              key: "pagination",
              label: "페이지",
              desc: "행이 많을 때 페이지 단위로 끊어 표시합니다 (기본 100행/페이지).\n한 페이지당 행 수는 별도 설정 가능 (10/20/50/100).\n총 데이터가 1,000행+ 인 마스터 테이블에 권장.",
            },
          ]}
          source={config.features as any}
          onToggle={updateFeatures}
        />
        <div style={{ marginTop: 8 }}>
          <FeatureChipGrid
            items={[
              {
                key: "compact",
                label: "컴팩트",
                desc: "행 높이를 좁게 (기본 36px → 28px) 표시합니다.\n한 화면에 더 많은 행을 보여줘 정보 밀도가 높아요.\n관리자 화면, 데이터 많은 조회 화면에 적합.",
              },
              {
                key: "borderless",
                label: "테두리 X",
                desc: "셀 사이의 세로 보더(구분선)를 제거하고 행간 zebra stripe(짝수행 음영)만 남깁니다.\n시각적으로 더 미니멀하지만 컬럼 구분이 약해져 — 컬럼 적은 표 (5개 이하) 에 적합.",
              },
            ]}
            source={config.style as any}
            onToggle={updateStyle}
          />
        </div>
      </CPGroup>

      {/* ── ▾ 컬럼 구성 ─────────────────────────── */}
      <CPGroup title="컬럼 구성" defaultOpen={false}>
        <ColumnPickerSection
          isModalMode={isModalMode}
          loadingSourceColumns={loadingSourceColumns}
          sourceTableName={config.data_source?.source_table}
          sourceTableColumns={sourceTableColumns}
          isSourceColumnSelected={isSourceColumnSelected}
          toggleSourceDisplayColumn={toggleSourceDisplayColumn}
          targetTableForColumns={targetTableForColumns}
          loadingColumns={loadingColumns}
          inputableColumns={inputableColumns}
          isColumnAdded={isColumnAdded}
          toggleInputColumn={toggleInputColumn}
        />

        <SelectedColumnsSection
          columns={config.columns}
          expandedColumn={expandedColumn}
          setExpandedColumn={setExpandedColumn}
          updateColumnProp={updateColumnProp}
          removeColumn={removeColumn}
          moveColumn={moveColumn}
          parentMenus={parentMenus}
          loadingMenus={loadingMenus}
          numberingRules={numberingRules}
          loadingNumberingRules={loadingNumberingRules}
          selectedMenuObjid={selectedMenuObjid}
          setSelectedMenuObjid={setSelectedMenuObjid}
        />
      </CPGroup>

      {/* ── ▾ 고급 설정 ─────────────────────────── */}
      <CPGroup title="고급 설정" defaultOpen={false}>
        {!isSourceDetail && (
          <CPRow label="소스 디테일 자동 조회" help="마스터 데이터의 자식 행 자동 채움">
            <CPSwitch
              value={!!config.source_detail_config}
              onChange={(checked) => {
                if (checked)
                  updateConfig({
                    source_detail_config: { table_name: "", foreign_key: "", parent_key: "" },
                  });
                else updateConfig({ source_detail_config: undefined });
              }}
            />
          </CPRow>
        )}
        {!isSourceDetail && config.source_detail_config && (
          <SourceDetailEditor
            value={config.source_detail_config}
            allTables={allTables}
            onChange={(next) => updateConfig({ source_detail_config: next })}
          />
        )}

        {(isModalMode || isInlineMode) && config.columns.length > 0 && (
          <CalcRulesEditor
            calcRules={calcRules}
            columns={config.columns}
            onAdd={addCalcRule}
            onRemove={removeCalcRule}
            onUpdate={updateCalcRule}
            onInsertCol={insertColToFormula}
            formulaToKorean={formulaToKorean}
          />
        )}

        <EntityJoinEditor
          loading={loadingEntityJoins}
          entityJoinData={entityJoinData}
          entityJoinTargetTable={entityJoinTargetTable}
          isEntityJoinColumnActive={isEntityJoinColumnActive}
          toggleEntityJoinColumn={toggleEntityJoinColumn}
          configuredJoins={config.entity_joins || []}
          onRemoveJoin={(idx) =>
            updateConfig({ entity_joins: (config.entity_joins || []).filter((_, i) => i !== idx) })
          }
        />

        <FeatureChipGrid
          items={[
            {
              key: "row_lock",
              label: "행 잠금",
              desc: "한 번 저장(commit) 된 행은 편집/삭제가 불가능해집니다.\n변경 이력이 중요한 회계, 공정 실적, 결재 데이터 등 감사(audit) 가 필요한 화면에 권장.\n새로 추가한 행은 첫 저장 전까지는 자유롭게 편집 가능해요.",
            },
            {
              key: "auto_save",
              label: "자동 저장",
              desc: "셀 변경 직후 백엔드에 PATCH 가 자동 호출됩니다.\n별도 [저장] 버튼이 필요 없어 빠르지만, 네트워크 오류 시 데이터 손실 가능성이 있어요.\n현재는 실험적 (experimental) — 핵심 데이터에는 권장하지 않음.",
            },
            {
              key: "export_csv",
              label: "CSV 출력",
              desc: "리피터 우상단에 ⤓ 다운로드 아이콘이 표시됩니다.\n클릭 시 현재 표의 행 데이터를 UTF-8 BOM CSV 로 다운로드.\n엑셀에서 바로 열리며 한글도 깨지지 않아요.",
            },
            {
              key: "no_duplicate_pick",
              label: "중복 방지",
              default: true,
              desc: "modal pick-many 모드에서 이미 추가된 행은 모달 검색 결과에서 회색 처리되어 중복 선택을 막습니다.\n예: 발주서에 같은 품목을 두 번 넣지 못하게 방어.\n기본 ON 권장.",
            },
          ]}
          source={config.features as any}
          onToggle={updateFeatures}
        />
        <CPRow label="최대 높이 (px)">
          <CPNumber
            value={config.style?.max_height ? Number(String(config.style.max_height).replace(/\D/g, "")) || undefined : undefined}
            onChange={(v) => updateStyle("max_height", v ? `${v}px` : undefined)}
            placeholder="자동"
          />
        </CPRow>
        <CPRow label="최소 높이 (px)">
          <CPNumber
            value={config.style?.min_height ? Number(String(config.style.min_height).replace(/\D/g, "")) || undefined : undefined}
            onChange={(v) => updateStyle("min_height", v ? `${v}px` : undefined)}
            placeholder="자동"
          />
        </CPRow>
      </CPGroup>
    </div>
  );
};

InvRepeaterConfigPanel.displayName = "InvRepeaterConfigPanel";

// ───────────────────────────────────────────────────────
// SourceDetailEditor — 소스 디테일 자동 조회 sub-config
// ───────────────────────────────────────────────────────
function SourceDetailEditor({
  value,
  allTables,
  onChange,
}: {
  value: V2RepeaterConfig["source_detail_config"];
  allTables: Array<{ tableName: string; displayName: string }>;
  onChange: (next: V2RepeaterConfig["source_detail_config"]) => void;
}) {
  if (!value) return null;
  return (
    <div
      style={{
        marginTop: 6,
        marginLeft: 6,
        paddingLeft: 10,
        borderLeft: "2px solid rgba(var(--v5-primary-rgb), 0.3)",
      }}
    >
      <CPRow label="디테일 테이블">
        <CPSelect
          value={value.table_name || ""}
          onChange={(v) => onChange({ ...value, table_name: v })}
        >
          <option value="">테이블 선택</option>
          {allTables.map((t) => (
            <option key={t.tableName} value={t.tableName}>
              {t.displayName}
            </option>
          ))}
        </CPSelect>
      </CPRow>
      <CPRow label="디테일 FK">
        <CPText
          value={value.foreign_key || ""}
          onChange={(v) => onChange({ ...value, foreign_key: v })}
          placeholder="order_no"
        />
      </CPRow>
      <CPRow label="마스터 키">
        <CPText
          value={value.parent_key || ""}
          onChange={(v) => onChange({ ...value, parent_key: v })}
          placeholder="order_no"
        />
      </CPRow>
      <Hint>
        마스터에서 [{value.parent_key || "?"}] → {value.table_name || "?"}.
        {value.foreign_key || "?"} 로 조회
      </Hint>
    </div>
  );
}

// ───────────────────────────────────────────────────────
// ColumnPickerSection — 입력/소스 컬럼 토글
// ───────────────────────────────────────────────────────
function ColumnPickerSection({
  isModalMode,
  loadingSourceColumns,
  sourceTableName,
  sourceTableColumns,
  isSourceColumnSelected,
  toggleSourceDisplayColumn,
  targetTableForColumns,
  loadingColumns,
  inputableColumns,
  isColumnAdded,
  toggleInputColumn,
}: {
  isModalMode: boolean;
  loadingSourceColumns: boolean;
  sourceTableName?: string;
  sourceTableColumns: ColumnOption[];
  isSourceColumnSelected: (n: string) => boolean;
  toggleSourceDisplayColumn: (c: ColumnOption) => void;
  targetTableForColumns?: string;
  loadingColumns: boolean;
  inputableColumns: ColumnOption[];
  isColumnAdded: (n: string) => boolean;
  toggleInputColumn: (c: ColumnOption) => void;
}) {
  return (
    <>
      <Hint>
        {isModalMode
          ? "소스 테이블 컬럼은 표시용, 저장 테이블 컬럼은 입력용"
          : "체크한 컬럼이 리피터에 입력 필드로 표시"}
      </Hint>

      {isModalMode && sourceTableName && (
        <div style={{ marginTop: 8 }}>
          <SectionLabel icon={<Link2 size={11} />} text={`소스: ${sourceTableName} · 표시용`} primary />
          {loadingSourceColumns ? (
            <InlineLoader text="로딩..." />
          ) : sourceTableColumns.length === 0 ? (
            <Hint>컬럼 정보가 없습니다</Hint>
          ) : (
            <ChipPickerBox>
              {sourceTableColumns.map((col) => (
                <CpChip
                  key={`src-${col.columnName}`}
                  active={isSourceColumnSelected(col.columnName)}
                  onClick={() => toggleSourceDisplayColumn(col)}
                >
                  {col.displayName}
                </CpChip>
              ))}
            </ChipPickerBox>
          )}
        </div>
      )}

      <div style={{ marginTop: 8 }}>
        <SectionLabel
          icon={<Database size={11} />}
          text={`저장: ${targetTableForColumns || "미선택"} · 입력용`}
        />
        {loadingColumns ? (
          <InlineLoader text="로딩..." />
        ) : inputableColumns.length === 0 ? (
          <Hint>컬럼 정보가 없습니다</Hint>
        ) : (
          <ChipPickerBox>
            {inputableColumns.map((col) => (
              <CpChip
                key={`in-${col.columnName}`}
                active={isColumnAdded(col.columnName)}
                onClick={() => toggleInputColumn(col)}
              >
                {col.displayName}
                <span style={{ opacity: 0.55, fontSize: 9.5, marginLeft: 4 }}>
                  {col.input_type}
                </span>
              </CpChip>
            ))}
          </ChipPickerBox>
        )}
      </div>
    </>
  );
}

function SectionLabel({ icon, text, primary }: { icon?: React.ReactNode; text: string; primary?: boolean }) {
  return (
    <div
      style={{
        display: "inline-flex",
        alignItems: "center",
        gap: 5,
        marginBottom: 5,
        fontSize: 10.5,
        fontWeight: 600,
        color: primary ? "var(--v5-primary, #6c5ce7)" : "var(--cp-text-muted)",
      }}
    >
      {icon} {text}
    </div>
  );
}

function ChipPickerBox({ children }: { children: React.ReactNode }) {
  return (
    <div
      style={{
        display: "flex",
        flexWrap: "wrap",
        gap: 4,
        padding: 8,
        background: "var(--cp-bg-subtle)",
        border: "1px solid var(--cp-border-subtle)",
        borderRadius: 6,
      }}
    >
      {children}
    </div>
  );
}

// ───────────────────────────────────────────────────────
// SelectedColumnsSection — 선택된 컬럼 카드 + 자동입력
// ───────────────────────────────────────────────────────
function SelectedColumnsSection({
  columns,
  expandedColumn,
  setExpandedColumn,
  updateColumnProp,
  removeColumn,
  moveColumn,
  parentMenus,
  loadingMenus,
  numberingRules,
  loadingNumberingRules,
  selectedMenuObjid,
  setSelectedMenuObjid,
}: {
  columns: RepeaterColumnConfig[];
  expandedColumn: string | null;
  setExpandedColumn: (k: string | null) => void;
  updateColumnProp: (key: string, field: keyof RepeaterColumnConfig, value: any) => void;
  removeColumn: (key: string) => void;
  moveColumn: (from: number, to: number) => void;
  parentMenus: any[];
  loadingMenus: boolean;
  numberingRules: NumberingRuleConfig[];
  loadingNumberingRules: boolean;
  selectedMenuObjid: number | undefined;
  setSelectedMenuObjid: (n: number | undefined) => void;
}) {
  if (columns.length === 0) {
    return <Hint>아직 선택된 컬럼이 없어요. 위에서 컬럼을 추가하세요.</Hint>;
  }
  return (
    <div style={{ marginTop: 10 }}>
      <SectionLabel text={`선택된 컬럼 ${columns.length}개 · 드래그로 순서 변경`} />
      <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
        {columns.map((col, idx) => (
          <div key={col.key} style={{ display: "flex", flexDirection: "column", gap: 3 }}>
            <div
              draggable
              onDragStart={(e) => e.dataTransfer.setData("colIdx", String(idx))}
              onDragOver={(e) => e.preventDefault()}
              onDrop={(e) => {
                e.preventDefault();
                const from = parseInt(e.dataTransfer.getData("colIdx"), 10);
                if (!isNaN(from)) moveColumn(from, idx);
              }}
              style={{
                display: "flex",
                alignItems: "center",
                gap: 6,
                padding: "5px 7px",
                background: col.is_source_display
                  ? "rgba(var(--v5-primary-rgb), 0.06)"
                  : col.is_join_column
                    ? "rgba(var(--v5-primary-rgb), 0.04)"
                    : "var(--cp-bg-subtle)",
                border: `1px solid ${
                  col.is_source_display || col.is_join_column
                    ? "rgba(var(--v5-primary-rgb), 0.25)"
                    : "var(--cp-border-subtle)"
                }`,
                borderRadius: 5,
                opacity: col.hidden ? 0.55 : 1,
              }}
            >
              <span style={{ cursor: "grab", color: "var(--cp-text-muted)", fontSize: 11 }}>⋮⋮</span>
              {!col.is_source_display && !col.is_join_column && (
                <button
                  type="button"
                  onClick={() => setExpandedColumn(expandedColumn === col.key ? null : col.key)}
                  style={{
                    width: 16,
                    height: 16,
                    border: "none",
                    background: "transparent",
                    color: "var(--cp-text-muted)",
                    cursor: "pointer",
                    fontSize: 10,
                  }}
                >
                  {expandedColumn === col.key ? "▾" : "▸"}
                </button>
              )}
              {col.is_source_display ? (
                <Link2 size={11} style={{ color: "var(--v5-primary, #6c5ce7)", flexShrink: 0 }} />
              ) : col.is_join_column ? (
                <Link2 size={11} style={{ color: "var(--cp-text-muted)", flexShrink: 0 }} />
              ) : (
                <Database size={11} style={{ color: "var(--cp-text-muted)", flexShrink: 0 }} />
              )}
              <input
                type="text"
                value={col.title}
                onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
                style={{
                  flex: 1,
                  height: 22,
                  padding: "0 6px",
                  fontSize: 11.5,
                  border: "1px solid transparent",
                  background: "transparent",
                  color: "var(--cp-text)",
                  outline: "none",
                  fontFamily: "var(--v5-font-sans)",
                  borderRadius: 3,
                }}
                onFocus={(e) => (e.currentTarget.style.background = "var(--cp-surface)")}
                onBlur={(e) => (e.currentTarget.style.background = "transparent")}
              />
              {!col.is_source_display && !col.is_join_column && (
                <>
                  <CpChip
                    active={!col.hidden}
                    onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
                  >
                    {col.hidden ? "히든" : "표시"}
                  </CpChip>
                  <CpChip
                    active={col.editable ?? true}
                    onClick={() => updateColumnProp(col.key, "editable", !(col.editable ?? true))}
                  >
                    {(col.editable ?? true) ? "편집" : "읽기"}
                  </CpChip>
                  {col.auto_fill?.type && col.auto_fill.type !== "none" && (
                    <Wand2 size={10} style={{ color: "rgb(168, 85, 247)" }} />
                  )}
                </>
              )}
              <CPIconBtn tone="danger" size={20} onClick={() => removeColumn(col.key)}>
                <Trash2 size={10} />
              </CPIconBtn>
            </div>

            {!col.is_source_display && !col.is_join_column && expandedColumn === col.key && (
              <AutoFillEditor
                col={col}
                updateColumnProp={updateColumnProp}
                parentMenus={parentMenus}
                loadingMenus={loadingMenus}
                numberingRules={numberingRules}
                loadingNumberingRules={loadingNumberingRules}
                selectedMenuObjid={selectedMenuObjid}
                setSelectedMenuObjid={setSelectedMenuObjid}
              />
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────
// AutoFillEditor — 자동 입력 sub-config
// ───────────────────────────────────────────────────────
function AutoFillEditor({
  col,
  updateColumnProp,
  parentMenus,
  loadingMenus,
  numberingRules,
  loadingNumberingRules,
  selectedMenuObjid,
  setSelectedMenuObjid,
}: {
  col: RepeaterColumnConfig;
  updateColumnProp: (key: string, field: keyof RepeaterColumnConfig, value: any) => void;
  parentMenus: any[];
  loadingMenus: boolean;
  numberingRules: NumberingRuleConfig[];
  loadingNumberingRules: boolean;
  selectedMenuObjid: number | undefined;
  setSelectedMenuObjid: (n: number | undefined) => void;
}) {
  return (
    <div
      style={{
        marginLeft: 22,
        padding: "6px 8px",
        background: "var(--cp-bg-subtle)",
        border: "1px dashed var(--cp-border-subtle)",
        borderRadius: 4,
      }}
    >
      <CPRow label="자동 입력">
        <CPSelect
          value={col.auto_fill?.type || "none"}
          onChange={(v) => {
            const next = v === "none" ? undefined : { type: v as any };
            updateColumnProp(col.key, "auto_fill", next);
          }}
        >
          {AUTO_FILL_OPTIONS.map((o) => (
            <option key={o.value} value={o.value}>
              {o.label}
            </option>
          ))}
        </CPSelect>
      </CPRow>

      {col.auto_fill?.type === "numbering" && (
        <>
          <CPRow label="대상 메뉴">
            <CPSelect
              value={selectedMenuObjid ? String(selectedMenuObjid) : ""}
              onChange={(v) => {
                const n = parseInt(v);
                setSelectedMenuObjid(n);
                updateColumnProp(col.key, "auto_fill", {
                  ...col.auto_fill,
                  selected_menu_objid: n,
                  numbering_rule_id: undefined,
                });
              }}
              disabled={loadingMenus}
            >
              <option value="">{loadingMenus ? "메뉴 로딩..." : "메뉴 선택"}</option>
              {parentMenus.map((m) => (
                <option key={m.objid} value={String(m.objid)}>
                  {m.menu_name_kor}
                </option>
              ))}
            </CPSelect>
          </CPRow>
          {selectedMenuObjid && (
            <CPRow label="채번 규칙">
              {loadingNumberingRules ? (
                <InlineLoader text="규칙 로딩..." />
              ) : numberingRules.length === 0 ? (
                <Hint tone="warn">선택된 메뉴에 채번 규칙이 없어요</Hint>
              ) : (
                <CPSelect
                  value={col.auto_fill?.numbering_rule_id || ""}
                  onChange={(v) =>
                    updateColumnProp(col.key, "auto_fill", {
                      ...col.auto_fill,
                      selected_menu_objid: selectedMenuObjid,
                      numbering_rule_id: v,
                    })
                  }
                >
                  <option value="">규칙 선택</option>
                  {numberingRules.map((r) => (
                    <option key={r.rule_id} value={r.rule_id}>
                      {r.rule_name}
                    </option>
                  ))}
                </CPSelect>
              )}
            </CPRow>
          )}
          {col.auto_fill?.numbering_rule_id && <Hint tone="ok">저장 시 채번 API 가 자동 생성</Hint>}
        </>
      )}

      {col.auto_fill?.type === "fromMainForm" && (
        <CPRow label="복사할 필드명">
          <CPText
            value={col.auto_fill?.source_field || ""}
            onChange={(v) => updateColumnProp(col.key, "auto_fill", { ...col.auto_fill, source_field: v })}
            placeholder="order_no"
          />
        </CPRow>
      )}

      {col.auto_fill?.type === "fixed" && (
        <CPRow label="고정값">
          <CPText
            value={String(col.auto_fill?.fixed_value || "")}
            onChange={(v) => updateColumnProp(col.key, "auto_fill", { ...col.auto_fill, fixed_value: v })}
            placeholder="고정값"
          />
        </CPRow>
      )}

      {col.auto_fill?.type === "parentSequence" && (
        <>
          <CPRow label="부모 번호 필드">
            <CPText
              value={col.auto_fill?.parent_field || ""}
              onChange={(v) =>
                updateColumnProp(col.key, "auto_fill", { ...col.auto_fill, parent_field: v })
              }
              placeholder="work_order_no"
            />
          </CPRow>
          <CPRow label="구분자">
            <CPText
              value={col.auto_fill?.separator ?? "-"}
              onChange={(v) =>
                updateColumnProp(col.key, "auto_fill", { ...col.auto_fill, separator: v })
              }
              placeholder="-"
            />
          </CPRow>
          <CPRow label="순번 자릿수">
            <CPNumber
              value={col.auto_fill?.sequence_length ?? 2}
              onChange={(v) =>
                updateColumnProp(col.key, "auto_fill", { ...col.auto_fill, sequence_length: v ?? 2 })
              }
              min={1}
              max={5}
            />
          </CPRow>
          <Hint tone="ok">
            예: WO-20260223-005{col.auto_fill?.separator ?? "-"}
            {String(1).padStart(col.auto_fill?.sequence_length ?? 2, "0")}
          </Hint>
        </>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────
// CalcRulesEditor — 계산 규칙
// ───────────────────────────────────────────────────────
function CalcRulesEditor({
  calcRules,
  columns,
  onAdd,
  onRemove,
  onUpdate,
  onInsertCol,
  formulaToKorean,
}: {
  calcRules: CalcRule[];
  columns: RepeaterColumnConfig[];
  onAdd: () => void;
  onRemove: (id: string) => void;
  onUpdate: (id: string, field: keyof CalcRule, value: string) => void;
  onInsertCol: (id: string, key: string) => void;
  formulaToKorean: (f: string) => string;
}) {
  return (
    <div style={{ marginTop: 10 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }}>
        <Calculator size={12} style={{ color: "var(--cp-text-muted)" }} />
        <SectionLabel text={`계산 규칙 ${calcRules.length}개`} />
        <button
          type="button"
          onClick={onAdd}
          style={{
            marginLeft: "auto",
            padding: "3px 8px",
            fontSize: 10.5,
            background: "var(--cp-bg-subtle)",
            border: "1px solid var(--cp-border)",
            borderRadius: 4,
            cursor: "pointer",
            color: "var(--cp-text)",
            display: "inline-flex",
            alignItems: "center",
            gap: 3,
          }}
        >
          <Plus size={10} /> 추가
        </button>
      </div>

      {calcRules.length === 0 ? (
        <Hint>아직 계산 규칙이 없어요. 위 추가 버튼으로 만들어보세요.</Hint>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
          {calcRules.map((r) => (
            <div
              key={r.id}
              style={{
                padding: "5px 6px",
                background: "var(--cp-bg-subtle)",
                border: "1px solid var(--cp-border-subtle)",
                borderRadius: 4,
              }}
            >
              <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
                <CPSelect
                  value={r.target_column}
                  onChange={(v) => onUpdate(r.id, "target_column", v)}
                  searchable={false}
                >
                  <option value="">결과</option>
                  {columns
                    .filter((c) => !c.is_source_display)
                    .map((c) => (
                      <option key={c.key} value={c.key}>
                        {c.title || c.key}
                      </option>
                    ))}
                </CPSelect>
                <span style={{ fontSize: 11 }}>=</span>
                <input
                  type="text"
                  value={r.formula}
                  onChange={(e) => onUpdate(r.id, "formula", e.target.value)}
                  placeholder="컬럼 클릭 또는 직접 입력"
                  style={{
                    flex: 1,
                    height: 22,
                    padding: "0 5px",
                    fontSize: 10.5,
                    fontFamily: "var(--v5-font-mono)",
                    background: "var(--cp-surface)",
                    border: "1px solid var(--cp-border)",
                    borderRadius: 3,
                    color: "var(--cp-text)",
                  }}
                />
                <CPIconBtn tone="danger" size={20} onClick={() => onRemove(r.id)}>
                  <Trash2 size={10} />
                </CPIconBtn>
              </div>

              {r.formula && (
                <Hint>
                  {columns.find((c) => c.key === r.target_column)?.title || r.target_column || "결과"} ={" "}
                  {formulaToKorean(r.formula)}
                </Hint>
              )}

              <div style={{ display: "flex", flexWrap: "wrap", gap: 3, marginTop: 4 }}>
                {columns
                  .filter((c) => c.key !== r.target_column && !c.is_source_display)
                  .map((c) => (
                    <CpChip key={c.key} onClick={() => onInsertCol(r.id, c.key)}>
                      {c.title || c.key}
                    </CpChip>
                  ))}
                {columns
                  .filter((c) => c.is_source_display)
                  .map((c) => (
                    <CpChip key={c.key} onClick={() => onInsertCol(r.id, c.key)} tone="primary">
                      {c.title || c.key}
                    </CpChip>
                  ))}
                {["+", "-", "*", "/", "(", ")"].map((op) => (
                  <CpChip key={op} onClick={() => onInsertCol(r.id, op)}>
                    {op}
                  </CpChip>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────
// EntityJoinEditor — 엔티티 조인
// ───────────────────────────────────────────────────────
function EntityJoinEditor({
  loading,
  entityJoinData,
  entityJoinTargetTable,
  isEntityJoinColumnActive,
  toggleEntityJoinColumn,
  configuredJoins,
  onRemoveJoin,
}: {
  loading: boolean;
  entityJoinData: {
    joinTables: Array<{
      tableName: string;
      currentDisplayColumn: string;
      joinConfig?: { sourceColumn?: string; source_column?: string };
      availableColumns: Array<{
        columnName: string;
        columnLabel: string;
        dataType: string;
        input_type?: string;
      }>;
    }>;
  };
  entityJoinTargetTable?: string;
  isEntityJoinColumnActive: (j: string, s: string, r: string) => boolean;
  toggleEntityJoinColumn: (
    j: string,
    s: string,
    r: string,
    label: string,
    field: string,
    type?: string,
  ) => void;
  configuredJoins: V2RepeaterConfig["entity_joins"];
  onRemoveJoin: (idx: number) => void;
}) {
  return (
    <div style={{ marginTop: 10 }}>
      <SectionLabel icon={<Link2 size={11} />} text="엔티티 조인 (FK 자동 표시)" primary />
      <Hint>FK 컬럼을 기반으로 참조 테이블 데이터를 자동 조회</Hint>

      {loading ? (
        <InlineLoader text="조인 가능 컬럼 찾는 중..." />
      ) : entityJoinData.joinTables.length === 0 ? (
        <Hint>
          {entityJoinTargetTable ? "조인 가능한 컬럼이 없어요" : "저장 테이블을 먼저 설정하세요"}
        </Hint>
      ) : (
        <div style={{ display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }}>
          {entityJoinData.joinTables.map((joinTable, ti) => {
            const sourceColumn =
              joinTable.joinConfig?.source_column || joinTable.joinConfig?.sourceColumn || "";
            return (
              <div
                key={ti}
                style={{
                  padding: 6,
                  background: "var(--cp-bg-subtle)",
                  border: "1px solid var(--cp-border-subtle)",
                  borderRadius: 4,
                }}
              >
                <div style={{ display: "flex", alignItems: "center", gap: 5, marginBottom: 5 }}>
                  <Link2 size={11} style={{ color: "var(--v5-primary, #6c5ce7)" }} />
                  <span style={{ fontSize: 11, fontWeight: 600 }}>{joinTable.tableName}</span>
                  <span style={{ fontSize: 10, color: "var(--cp-text-muted)" }}>
                    ({sourceColumn})
                  </span>
                </div>
                <ChipPickerBox>
                  {joinTable.availableColumns.map((c) => {
                    const active = isEntityJoinColumnActive(joinTable.tableName, sourceColumn, c.columnName);
                    return (
                      <CpChip
                        key={c.columnName}
                        active={active}
                        onClick={() =>
                          toggleEntityJoinColumn(
                            joinTable.tableName,
                            sourceColumn,
                            c.columnName,
                            c.columnLabel,
                            c.columnName,
                            c.input_type || c.dataType,
                          )
                        }
                      >
                        {c.columnLabel}
                      </CpChip>
                    );
                  })}
                </ChipPickerBox>
              </div>
            );
          })}
        </div>
      )}

      {configuredJoins && configuredJoins.length > 0 && (
        <div style={{ marginTop: 6 }}>
          <SectionLabel text={`설정된 조인 ${configuredJoins.length}개`} />
          <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
            {configuredJoins.map((j, idx) => (
              <div
                key={idx}
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 5,
                  padding: "4px 6px",
                  fontSize: 10.5,
                  background: "var(--cp-bg-subtle)",
                  border: "1px solid var(--cp-border-subtle)",
                  borderRadius: 4,
                }}
              >
                <Database size={10} style={{ color: "var(--v5-primary, #6c5ce7)" }} />
                <span style={{ fontWeight: 600 }}>{j.source_column}</span>
                <ArrowRight size={10} style={{ color: "var(--cp-text-muted)" }} />
                <span>{j.reference_table}</span>
                <span style={{ color: "var(--cp-text-muted)" }}>
                  ({j.columns.map((c) => c.reference_field).join(", ")})
                </span>
                <span style={{ flex: 1 }} />
                <CPIconBtn tone="danger" size={20} onClick={() => onRemoveJoin(idx)}>
                  <Trash2 size={10} />
                </CPIconBtn>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────
// FeatureChipGrid — 토글 묶음을 chip grid 로 표현
// (CPRow + CPSwitch row 8개 나열 = 이전 시스템과 똑같이 보임 → grid 로 평탄화)
//
// - 한 줄에 자동 N개 (auto-fill, minmax 96px)
// - 활성: primary 컬러 + 좌측 dot + 약한 글로우
// - 비활성: 회색 톤
// - 호버: tooltip (desc)
// - NEW 라벨: 우측 작은 배지
// ───────────────────────────────────────────────────────
interface FeatureChipItem {
  key: string;
  label: string;
  desc?: string;
  default?: boolean;
  isNew?: boolean;
}

const TIP_WIDTH = 280;
const TIP_PAD = 8;

// Group Cooldown 패턴 타이밍 (Stripe / Linear 표준)
const TIP_OPEN_DELAY = 500;     // 첫 호버 → tooltip 표시까지
const TIP_CLOSE_GRACE = 250;    // mouseLeave → 실제 닫힘까지 (다른 칩 이동 시 깜빡임 방지)
const TIP_GROUP_COOLDOWN = 500; // tooltip 닫힘 → group 해제까지 (이 동안 다시 호버 시 즉시 표시)

function FeatureChipGrid({
  items,
  source,
  onToggle,
}: {
  items: FeatureChipItem[];
  source?: Record<string, any>;
  onToggle: (key: string, value: boolean) => void;
}) {
  const [hoverIdx, setHoverIdx] = useState<number | null>(null);
  const [chipRect, setChipRect] = useState<{ left: number; center: number; top: number } | null>(null);
  const [tipVisible, setTipVisible] = useState(false);

  // Group Cooldown — group active 상태를 ref 로 (rerender 안 일으킴)
  const groupActiveRef = useRef(false);
  const openTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const cooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const clearTimer = (ref: React.MutableRefObject<ReturnType<typeof setTimeout> | null>) => {
    if (ref.current) {
      clearTimeout(ref.current);
      ref.current = null;
    }
  };

  const handleHoverEnter = useCallback((i: number, el: HTMLElement) => {
    // 진행 중인 close grace / group cooldown 취소
    clearTimer(closeTimerRef);
    clearTimer(cooldownTimerRef);

    const r = el.getBoundingClientRect();
    setHoverIdx(i);
    setChipRect({ left: r.left, center: r.left + r.width / 2, top: r.top });

    if (groupActiveRef.current) {
      // group active → 즉시 표시
      setTipVisible(true);
    } else {
      // 첫 호버 → open delay 후 표시
      clearTimer(openTimerRef);
      openTimerRef.current = setTimeout(() => {
        setTipVisible(true);
        groupActiveRef.current = true;
        openTimerRef.current = null;
      }, TIP_OPEN_DELAY);
    }
  }, []);

  const handleHoverLeave = useCallback(() => {
    // 아직 open delay 진행 중이었으면 cancel (사용자가 빠르게 지나감)
    clearTimer(openTimerRef);

    // close grace 후 닫기
    clearTimer(closeTimerRef);
    closeTimerRef.current = setTimeout(() => {
      setTipVisible(false);
      setHoverIdx(null);
      closeTimerRef.current = null;

      // group cooldown 후 해제 (이 동안 다시 호버하면 즉시 표시)
      clearTimer(cooldownTimerRef);
      cooldownTimerRef.current = setTimeout(() => {
        groupActiveRef.current = false;
        cooldownTimerRef.current = null;
      }, TIP_GROUP_COOLDOWN);
    }, TIP_CLOSE_GRACE);
  }, []);

  // unmount 시 모든 timer 정리
  useEffect(() => {
    return () => {
      clearTimer(openTimerRef);
      clearTimer(closeTimerRef);
      clearTimer(cooldownTimerRef);
    };
  }, []);

  const onCount = items.filter((it) => (source?.[it.key] ?? it.default ?? false)).length;

  // hover 중인 칩의 데이터
  const hoverItem = hoverIdx !== null ? items[hoverIdx] : null;
  const hoverActive = hoverItem ? !!(source?.[hoverItem.key] ?? hoverItem.default ?? false) : false;

  // tooltip 위치 계산 (화면 좌우 boundary clamp + arrow 동적 정렬)
  let tipLeft = 0;
  let arrowLeft = TIP_WIDTH / 2;
  if (chipRect && typeof window !== "undefined") {
    const rawLeft = chipRect.center - TIP_WIDTH / 2;
    const minLeft = TIP_PAD;
    const maxLeft = window.innerWidth - TIP_WIDTH - TIP_PAD;
    tipLeft = Math.min(Math.max(rawLeft, minLeft), maxLeft);
    arrowLeft = chipRect.center - tipLeft;
    // arrow 가 tooltip 안쪽 적정 범위에만 있도록 (양 끝 12px 여유)
    arrowLeft = Math.min(Math.max(arrowLeft, 12), TIP_WIDTH - 12);
  }
  return (
    <div>
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 6,
          marginBottom: 5,
          fontSize: 10,
          color: "var(--cp-text-muted)",
          fontFamily: "var(--v5-font-mono)",
          letterSpacing: 0.02,
        }}
      >
        <span>활성</span>
        <span style={{ color: "var(--v5-primary, #6c5ce7)", fontWeight: 700 }}>{onCount}</span>
        <span>/ {items.length}</span>
      </div>
      <div
        style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))",
          gap: 5,
        }}
      >
        {items.map((it, i) => {
          const active = !!(source?.[it.key] ?? it.default ?? false);
          return (
            <button
              key={it.key}
              type="button"
              onClick={() => onToggle(it.key, !active)}
              onFocus={(e) => handleHoverEnter(i, e.currentTarget)}
              onBlur={handleHoverLeave}
              style={{
                position: "relative",
                display: "flex",
                alignItems: "center",
                gap: 6,
                padding: "6px 8px 6px 7px",
                background: active
                  ? "rgba(var(--v5-primary-rgb), 0.10)"
                  : "var(--cp-bg-subtle)",
                border: `1px solid ${
                  active
                    ? "rgba(var(--v5-primary-rgb), 0.45)"
                    : "var(--cp-border-subtle)"
                }`,
                borderRadius: 5,
                cursor: "pointer",
                fontFamily: "var(--v5-font-sans)",
                boxShadow: active
                  ? "0 0 0 2px rgba(var(--v5-primary-rgb), 0.06), 0 1px 2px rgba(0,0,0,0.03)"
                  : "none",
                transition: "all .14s ease",
                textAlign: "left",
                minHeight: 26,
              }}
              onMouseEnter={(e) => {
                handleHoverEnter(i, e.currentTarget as HTMLElement);
                if (!active) {
                  (e.currentTarget as HTMLButtonElement).style.background =
                    "var(--cp-surface-hover, var(--cp-surface))";
                }
              }}
              onMouseLeave={(e) => {
                handleHoverLeave();
                if (!active) {
                  (e.currentTarget as HTMLButtonElement).style.background =
                    "var(--cp-bg-subtle)";
                }
              }}
            >
              <span
                aria-hidden
                style={{
                  width: 6,
                  height: 6,
                  borderRadius: 999,
                  background: active
                    ? "var(--v5-primary, #6c5ce7)"
                    : "var(--cp-border-strong, var(--cp-border))",
                  boxShadow: active
                    ? "0 0 5px rgba(var(--v5-primary-rgb), 0.55)"
                    : "none",
                  flexShrink: 0,
                  transition: "all .14s ease",
                }}
              />
              <span
                style={{
                  flex: 1,
                  fontSize: 11,
                  fontWeight: active ? 600 : 500,
                  color: active ? "var(--cp-text)" : "var(--cp-text-sec)",
                  overflow: "hidden",
                  textOverflow: "ellipsis",
                  whiteSpace: "nowrap",
                  letterSpacing: "-0.005em",
                }}
              >
                {it.label}
              </span>
              {it.isNew && (
                <span
                  style={{
                    fontSize: 8.5,
                    fontWeight: 700,
                    padding: "1px 4px",
                    background: "rgba(var(--v5-primary-rgb), 0.15)",
                    color: "var(--v5-primary, #6c5ce7)",
                    borderRadius: 3,
                    letterSpacing: "0.04em",
                  }}
                >
                  NEW
                </span>
              )}
            </button>
          );
        })}
      </div>

      {/* Portal tooltip — body 에 렌더하여 부모 overflow 우회 */}
      {tipVisible && hoverItem && hoverItem.desc && chipRect && typeof document !== "undefined" &&
        createPortal(
          <div
            role="tooltip"
            style={{
              position: "fixed",
              top: chipRect.top - 8,
              left: tipLeft,
              transform: "translateY(-100%)",
              width: TIP_WIDTH,
              pointerEvents: "none",
              zIndex: 9999,
            }}
          >
            <div
              style={{
                position: "relative",
                background: "rgba(20, 18, 36, 0.97)",
                color: "#fff",
                padding: "8px 11px 7px 10px",
                borderRadius: 6,
                boxShadow:
                  "0 12px 32px rgba(0,0,0,0.42), 0 0 0 1px rgba(var(--v5-primary-rgb), 0.3)",
                fontFamily: "var(--v5-font-sans)",
                animation: "cp-fade-up 0.14s ease-out",
                lineHeight: 1.45,
              }}
            >
              {/* 1행: dot + 라벨 + NEW */}
              <div
                style={{
                  display: "flex",
                  alignItems: "center",
                  gap: 5,
                  marginBottom: 3,
                }}
              >
                <span
                  aria-hidden
                  style={{
                    width: 5,
                    height: 5,
                    borderRadius: 999,
                    background: hoverActive ? "#10b981" : "rgba(255,255,255,0.32)",
                    boxShadow: hoverActive ? "0 0 5px rgba(16,185,129,0.7)" : "none",
                    flexShrink: 0,
                  }}
                />
                <span
                  style={{
                    fontSize: 11.5,
                    fontWeight: 700,
                    color: "#fff",
                    letterSpacing: "-0.01em",
                    flex: 1,
                    whiteSpace: "nowrap",
                  }}
                >
                  {hoverItem.label}
                </span>
                {hoverItem.isNew && (
                  <span
                    style={{
                      fontSize: 8,
                      fontWeight: 700,
                      padding: "1px 4px",
                      background: "rgba(var(--v5-primary-rgb), 0.3)",
                      color: "#b8b3ff",
                      borderRadius: 3,
                      letterSpacing: "0.04em",
                      marginLeft: 2,
                    }}
                  >
                    NEW
                  </span>
                )}
              </div>

              {/* 2행: 설명 (줄바꿈 \n 표시) */}
              <div
                style={{
                  fontSize: 10.5,
                  color: "rgba(255,255,255,0.82)",
                  fontWeight: 400,
                  whiteSpace: "pre-line",
                  lineHeight: 1.55,
                }}
              >
                {hoverItem.desc}
              </div>

              {/* 3행: 상태 + key path (모노) */}
              <div
                style={{
                  marginTop: 5,
                  paddingTop: 4,
                  borderTop: "1px solid rgba(255,255,255,0.1)",
                  fontSize: 9.5,
                  color: "rgba(255,255,255,0.45)",
                  fontFamily: "var(--v5-font-mono)",
                  letterSpacing: 0.02,
                  display: "flex",
                  alignItems: "center",
                  gap: 5,
                }}
              >
                <span style={{ color: hoverActive ? "#10b981" : "rgba(255,255,255,0.42)" }}>
                  {hoverActive ? "● ON" : "○ OFF"}
                </span>
                <span style={{ opacity: 0.4 }}>·</span>
                <span>{hoverItem.key}</span>
              </div>

              {/* 화살표 ▾ (chip 중앙으로 동적 정렬) */}
              <span
                aria-hidden
                style={{
                  position: "absolute",
                  top: "100%",
                  left: arrowLeft,
                  transform: "translateX(-50%)",
                  width: 0,
                  height: 0,
                  borderLeft: "5px solid transparent",
                  borderRight: "5px solid transparent",
                  borderTop: "5px solid rgba(20, 18, 36, 0.97)",
                }}
              />
            </div>
          </div>,
          document.body,
        )}
    </div>
  );
}

export default InvRepeaterConfigPanel;
