"use client";

/**
 * pop-card-list 런타임 컴포넌트
 *
 * 테이블의 각 행이 하나의 카드로 표시됩니다.
 * 카드 구조:
 * - 헤더: 코드 + 제목
 * - 본문: 이미지(왼쪽) + 라벨-값 목록(오른쪽)
 */

import React, { useEffect, useState, useRef, useMemo, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Loader2, ChevronDown, ChevronUp, ChevronLeft, ChevronRight, ShoppingCart, X } from "lucide-react";
import * as LucideIcons from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import type {
  PopCardListConfig,
  CardTemplateConfig,
  CardFieldBinding,
  CardInputFieldConfig,
  CardCalculatedFieldConfig,
  CardCartActionConfig,
  CardPresetSpec,
  CartItem,
} from "../types";
import { DEFAULT_CARD_IMAGE, CARD_PRESET_SPECS } from "../types";
import { dataApi } from "@/lib/api/data";
import { usePopEvent } from "@/hooks/pop/usePopEvent";
import { NumberInputModal } from "./NumberInputModal";

// Lucide 아이콘 동적 렌더링
function DynamicLucideIcon({ name, size = 20 }: { name?: string; size?: number }) {
  if (!name) return <ShoppingCart size={size} />;
  const icons = LucideIcons as unknown as Record<string, React.ComponentType<{ size?: number }>>;
  const IconComp = icons[name];
  if (!IconComp) return <ShoppingCart size={size} />;
  return <IconComp size={size} />;
}

// 마퀴 애니메이션 keyframes (한 번만 삽입)
const MARQUEE_STYLE_ID = "pop-card-marquee-style";
if (typeof document !== "undefined" && !document.getElementById(MARQUEE_STYLE_ID)) {
  const style = document.createElement("style");
  style.id = MARQUEE_STYLE_ID;
  style.textContent = `
    @keyframes pop-marquee {
      0%, 15% { transform: translateX(0); }
      85%, 100% { transform: translateX(var(--marquee-offset)); }
    }
  `;
  document.head.appendChild(style);
}

// 텍스트가 컨테이너보다 넓을 때 자동 슬라이딩하는 컴포넌트
function MarqueeText({
  children,
  className,
  style,
}: {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const textRef = useRef<HTMLSpanElement>(null);
  const [overflowPx, setOverflowPx] = useState(0);

  const measure = useCallback(() => {
    const container = containerRef.current;
    const text = textRef.current;
    if (!container || !text) return;
    const diff = text.scrollWidth - container.clientWidth;
    setOverflowPx(diff > 1 ? diff : 0);
  }, []);

  useEffect(() => {
    measure();
  }, [children, measure]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const ro = new ResizeObserver(() => measure());
    ro.observe(container);
    return () => ro.disconnect();
  }, [measure]);

  return (
    <div ref={containerRef} className={`overflow-hidden ${className || ""}`} style={style}>
      <span
        ref={textRef}
        className="inline-block whitespace-nowrap"
        style={
          overflowPx > 0
            ? {
                ["--marquee-offset" as string]: `-${overflowPx}px`,
                animation: "pop-marquee 5s ease-in-out infinite alternate",
              }
            : undefined
        }
      >
        {children}
      </span>
    </div>
  );
}

interface PopCardListComponentProps {
  config?: PopCardListConfig;
  className?: string;
  screenId?: string;
  // 동적 크기 변경을 위한 props (PopRenderer에서 전달)
  componentId?: string;
  currentRowSpan?: number;
  currentColSpan?: number;
  onRequestResize?: (componentId: string, newRowSpan: number, newColSpan?: number) => void;
}

// 테이블 행 데이터 타입
type RowData = Record<string, unknown>;

// 카드 내부 스타일 규격 (프리셋에서 매핑)
interface ScaledConfig {
  cardHeight: number;
  cardWidth: number;
  imageSize: number;
  padding: number;
  gap: number;
  headerPaddingX: number;
  headerPaddingY: number;
  codeTextSize: number;
  titleTextSize: number;
  bodyTextSize: number;
}

export function PopCardListComponent({
  config,
  className,
  screenId,
  componentId,
  currentRowSpan,
  currentColSpan,
  onRequestResize,
}: PopCardListComponentProps) {
  const isHorizontalMode = (config?.scrollDirection || "vertical") === "horizontal";
  const maxGridColumns = config?.gridColumns || 2;
  const configGridRows = config?.gridRows || 3;
  const dataSource = config?.dataSource;
  const template = config?.cardTemplate;

  // 이벤트 기반 company_code 필터링
  const [eventCompanyCode, setEventCompanyCode] = useState<string | undefined>();
  const { subscribe, publish, getSharedData, setSharedData } = usePopEvent(screenId || "default");
  const router = useRouter();

  useEffect(() => {
    if (!screenId) return;
    const unsub = subscribe("company_selected", (payload: unknown) => {
      const p = payload as { companyCode?: string } | undefined;
      setEventCompanyCode(p?.companyCode);
    });
    return unsub;
  }, [screenId, subscribe]);

  // 데이터 상태
  const [rows, setRows] = useState<RowData[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // 확장/페이지네이션 상태
  const [isExpanded, setIsExpanded] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const [originalRowSpan, setOriginalRowSpan] = useState<number | null>(null);

  // 컨테이너 ref + 크기 측정
  const containerRef = useRef<HTMLDivElement>(null);
  const [containerWidth, setContainerWidth] = useState(0);
  const [containerHeight, setContainerHeight] = useState(0);
  const baseContainerHeight = useRef(0);

  useEffect(() => {
    if (!containerRef.current) return;
    const observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      if (width > 0) setContainerWidth(width);
      if (height > 0) setContainerHeight(height);
    });
    observer.observe(containerRef.current);
    return () => observer.disconnect();
  }, []);

  // 이미지 URL 없는 항목 카운트 (toast 중복 방지용)
  const missingImageCountRef = useRef(0);
  const toastShownRef = useRef(false);

  const spec: CardPresetSpec = CARD_PRESET_SPECS.large;

  // 브레이크포인트별 열 제한: colSpan >= 8이면 config 최대값 허용, 그 외 1열
  const maxAllowedColumns = useMemo(() => {
    if (!currentColSpan) return maxGridColumns;
    if (currentColSpan >= 8) return maxGridColumns;
    return 1;
  }, [currentColSpan, maxGridColumns]);

  // 카드 최소 너비 기준으로 컨테이너에 들어갈 수 있는 열 개수 자동 계산
  const minCardWidth = Math.round(spec.height * 1.6);
  const autoColumns =
    containerWidth > 0
      ? Math.max(1, Math.floor((containerWidth + spec.gap) / (minCardWidth + spec.gap)))
      : maxGridColumns;
  const gridColumns = Math.min(autoColumns, maxGridColumns, maxAllowedColumns);

  // 컨테이너 높이 기반 행 수 자동 계산 (잘림 방지)
  const effectiveGridRows = useMemo(() => {
    if (containerHeight <= 0) return configGridRows;

    const controlBarHeight = 44;
    const effectiveHeight = baseContainerHeight.current > 0 ? baseContainerHeight.current : containerHeight;
    const availableHeight = effectiveHeight - controlBarHeight;

    const cardHeightWithGap = spec.height + spec.gap;
    const fittableRows = Math.max(1, Math.floor((availableHeight + spec.gap) / cardHeightWithGap));

    return Math.min(configGridRows, fittableRows);
  }, [containerHeight, configGridRows, spec]);

  const gridRows = effectiveGridRows;

  // 카드 크기: 컨테이너 실측 크기에서 gridColumns x gridRows 기준으로 동적 계산
  const scaled = useMemo((): ScaledConfig => {
    const gap = spec.gap;
    const controlBarHeight = 44;

    const buildScaledConfig = (cardWidth: number, cardHeight: number): ScaledConfig => {
      const scale = cardHeight / spec.height;
      return {
        cardHeight,
        cardWidth,
        imageSize: Math.round(spec.imageSize * scale),
        padding: Math.round(spec.padding * scale),
        gap,
        headerPaddingX: Math.round(spec.headerPadX * scale),
        headerPaddingY: Math.round(spec.headerPadY * scale),
        codeTextSize: Math.round(spec.codeText * scale),
        titleTextSize: Math.round(spec.titleText * scale),
        bodyTextSize: Math.round(spec.bodyText * scale),
      };
    };

    if (containerWidth <= 0 || containerHeight <= 0) {
      return buildScaledConfig(Math.round(spec.height * 1.6), spec.height);
    }

    const effectiveHeight = baseContainerHeight.current > 0 ? baseContainerHeight.current : containerHeight;

    const availableHeight = effectiveHeight - controlBarHeight;
    const availableWidth = containerWidth;

    const cardHeight = Math.max(spec.height, Math.floor((availableHeight - gap * (gridRows - 1)) / gridRows));
    const cardWidth = Math.max(
      Math.round(spec.height * 1.6),
      Math.floor((availableWidth - gap * (gridColumns - 1)) / gridColumns),
    );

    return buildScaledConfig(cardWidth, cardHeight);
  }, [spec, containerWidth, containerHeight, gridColumns, gridRows]);

  // 기본 상태에서 표시할 카드 수
  const visibleCardCount = useMemo(() => {
    return gridColumns * gridRows;
  }, [gridColumns, gridRows]);

  // 더보기 버튼 표시 여부
  const hasMoreCards = rows.length > visibleCardCount;

  // 확장 상태에서 표시할 카드 수 계산
  const expandedCardsPerPage = useMemo(() => {
    // 가로/세로 모두: 기본 표시 수의 2배 + 스크롤 유도를 위해 1줄 추가
    // 가로: 컴포넌트 크기 변경 없이 카드 2배 → 가로 스크롤로 탐색
    // 세로: rowSpan 2배 → 2배 영역에 카드 채움 + 세로 스크롤
    return Math.max(1, visibleCardCount * 2 + gridColumns);
  }, [visibleCardCount, gridColumns]);

  // 스크롤 영역 ref
  const scrollAreaRef = useRef<HTMLDivElement>(null);

  // 현재 표시할 카드 결정
  const displayCards = useMemo(() => {
    if (!isExpanded) {
      // 기본 상태: visibleCardCount만큼만 표시
      return rows.slice(0, visibleCardCount);
    } else {
      // 확장 상태: 현재 페이지의 카드들 (스크롤 가능하도록 더 많이)
      const start = (currentPage - 1) * expandedCardsPerPage;
      const end = start + expandedCardsPerPage;
      return rows.slice(start, end);
    }
  }, [rows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage]);

  // 총 페이지 수
  const totalPages = isExpanded ? Math.ceil(rows.length / expandedCardsPerPage) : 1;
  // 페이지네이션 필요: 확장 상태에서 전체 카드가 한 페이지를 초과할 때
  const needsPagination = isExpanded && totalPages > 1;

  // 페이지 변경 핸들러
  const handlePrevPage = () => {
    if (currentPage > 1) {
      setCurrentPage(currentPage - 1);
    }
  };

  const handleNextPage = () => {
    if (currentPage < totalPages) {
      setCurrentPage(currentPage + 1);
    }
  };

  // 확장/접기 토글: 세로 모드에서만 rowSpan 2배 확장, 가로 모드에서는 크기 변경 없이 카드만 추가 표시
  const toggleExpand = () => {
    if (isExpanded) {
      if (!isHorizontalMode && originalRowSpan !== null && componentId && onRequestResize) {
        onRequestResize(componentId, originalRowSpan);
      }
      setCurrentPage(1);
      setOriginalRowSpan(null);
      baseContainerHeight.current = 0;
      setIsExpanded(false);
    } else {
      baseContainerHeight.current = containerHeight;
      if (!isHorizontalMode && componentId && onRequestResize && currentRowSpan !== undefined) {
        setOriginalRowSpan(currentRowSpan);
        onRequestResize(componentId, currentRowSpan * 2);
      }
      setIsExpanded(true);
    }
  };

  // 페이지 변경 시 스크롤 위치 초기화 (가로/세로 모두)
  useEffect(() => {
    if (scrollAreaRef.current && isExpanded) {
      scrollAreaRef.current.scrollTop = 0;
      scrollAreaRef.current.scrollLeft = 0;
    }
  }, [currentPage, isExpanded]);

  // 데이터 조회
  useEffect(() => {
    if (!dataSource?.tableName) {
      setLoading(false);
      setRows([]);
      return;
    }

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      missingImageCountRef.current = 0;
      toastShownRef.current = false;

      try {
        // 필터 조건 구성
        const filters: Record<string, unknown> = {};
        if (dataSource.filters && dataSource.filters.length > 0) {
          dataSource.filters.forEach((f) => {
            if (f.column && f.value) {
              filters[f.column] = f.value;
            }
          });
        }

        // 이벤트로 수신한 company_code 필터 병합
        if (eventCompanyCode) {
          filters["company_code"] = eventCompanyCode;
        }

        // 정렬 조건
        const sortBy = dataSource.sort?.column;
        const sortOrder = dataSource.sort?.direction;

        // 개수 제한
        const size = dataSource.limit?.mode === "limited" && dataSource.limit?.count ? dataSource.limit.count : 100;

        // TODO: 조인 지원은 추후 구현
        // 현재는 단일 테이블 조회만 지원

        const result = await dataApi.getTableData(dataSource.tableName, {
          page: 1,
          size,
          sortBy: sortOrder ? sortBy : undefined,
          sortOrder,
          filters: Object.keys(filters).length > 0 ? filters : undefined,
        });

        setRows(result.data || []);
      } catch (err) {
        const message = err instanceof Error ? err.message : "데이터 조회 실패";
        setError(message);
        setRows([]);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [dataSource, eventCompanyCode]);

  // 이미지 URL 없는 항목 체크 및 toast 표시
  useEffect(() => {
    if (
      !loading &&
      rows.length > 0 &&
      template?.image?.enabled &&
      template?.image?.imageColumn &&
      !toastShownRef.current
    ) {
      const imageColumn = template.image.imageColumn;
      const missingCount = rows.filter((row) => !row[imageColumn]).length;

      if (missingCount > 0) {
        missingImageCountRef.current = missingCount;
        toastShownRef.current = true;
        toast.warning(`${missingCount}개 항목의 이미지 URL이 없어 기본 이미지로 표시됩니다`);
      }
    }
  }, [loading, rows, template?.image]);

  // 카드 영역 스타일
  const cardAreaStyle: React.CSSProperties = {
    gap: `${scaled.gap}px`,
    ...(isHorizontalMode
      ? {
          gridTemplateRows: `repeat(${gridRows}, ${scaled.cardHeight}px)`,
          gridAutoFlow: "column",
          gridAutoColumns: `${scaled.cardWidth}px`,
        }
      : {
          // 세로 모드: 1fr 비율 기반으로 컨테이너 너비 초과 방지
          gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
          gridAutoRows: `${scaled.cardHeight}px`,
        }),
  };

  // 세로 모드 스크롤 클래스: 비확장 시 overflow hidden, 확장 시에만 세로 스크롤 허용
  const scrollClassName = isHorizontalMode
    ? "overflow-x-auto overflow-y-hidden"
    : isExpanded
      ? "overflow-y-auto overflow-x-hidden"
      : "overflow-hidden";

  return (
    <div ref={containerRef} className={`flex h-full w-full flex-col ${className || ""}`}>
      {!dataSource?.tableName ? (
        <div className="bg-muted/30 flex flex-1 items-center justify-center rounded-md border border-dashed p-4">
          <p className="text-muted-foreground text-sm">데이터 소스를 설정해주세요.</p>
        </div>
      ) : loading ? (
        <div className="bg-muted/30 flex flex-1 items-center justify-center rounded-md border p-4">
          <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
        </div>
      ) : error ? (
        <div className="border-destructive/50 bg-destructive/10 flex flex-1 items-center justify-center rounded-md border p-4">
          <p className="text-destructive text-sm">{error}</p>
        </div>
      ) : rows.length === 0 ? (
        <div className="bg-muted/30 flex flex-1 items-center justify-center rounded-md border border-dashed p-4">
          <p className="text-muted-foreground text-sm">데이터가 없습니다.</p>
        </div>
      ) : (
        <>
          {/* 카드 영역 (스크롤 가능) */}
          <div
            ref={scrollAreaRef}
            className={`grid min-h-0 flex-1 ${scrollClassName}`}
            style={{
              ...cardAreaStyle,
              alignContent: "start",
              justifyContent: isHorizontalMode ? "start" : "center",
            }}
          >
            {displayCards.map((row, index) => (
              <Card
                key={index}
                row={row}
                template={template}
                scaled={scaled}
                inputField={config?.inputField}
                calculatedField={config?.calculatedField}
                cartAction={config?.cartAction}
                publish={publish}
                getSharedData={getSharedData}
                setSharedData={setSharedData}
                router={router}
              />
            ))}
          </div>

          {/* 하단 컨트롤 영역 */}
          {hasMoreCards && (
            <div className="bg-background shrink-0 border-t px-3 py-2">
              <div className="flex items-center justify-between gap-2">
                <div className="flex items-center gap-2">
                  <Button variant="outline" size="sm" onClick={toggleExpand} className="h-9 px-4 text-sm font-medium">
                    {isExpanded ? (
                      <>
                        접기
                        <ChevronUp className="ml-1 h-4 w-4" />
                      </>
                    ) : (
                      <>
                        더보기
                        <ChevronDown className="ml-1 h-4 w-4" />
                      </>
                    )}
                  </Button>
                  <span className="text-muted-foreground text-xs">{rows.length}건</span>
                </div>

                {isExpanded && needsPagination && (
                  <div className="flex items-center gap-1.5">
                    <Button
                      variant="default"
                      size="sm"
                      onClick={handlePrevPage}
                      disabled={currentPage <= 1}
                      className="h-9 w-9 p-0"
                    >
                      <ChevronLeft className="h-5 w-5" />
                    </Button>
                    <span className="min-w-[48px] text-center text-sm font-medium">
                      {currentPage} / {totalPages}
                    </span>
                    <Button
                      variant="default"
                      size="sm"
                      onClick={handleNextPage}
                      disabled={currentPage >= totalPages}
                      className="h-9 w-9 p-0"
                    >
                      <ChevronRight className="h-5 w-5" />
                    </Button>
                  </div>
                )}
              </div>
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ===== 카드 컴포넌트 =====

function Card({
  row,
  template,
  scaled,
  inputField,
  calculatedField,
  cartAction,
  publish,
  getSharedData,
  setSharedData,
  router,
}: {
  row: RowData;
  template?: CardTemplateConfig;
  scaled: ScaledConfig;
  inputField?: CardInputFieldConfig;
  calculatedField?: CardCalculatedFieldConfig;
  cartAction?: CardCartActionConfig;
  publish: (eventName: string, payload?: unknown) => void;
  getSharedData: <T = unknown>(key: string) => T | undefined;
  setSharedData: (key: string, value: unknown) => void;
  router: ReturnType<typeof useRouter>;
}) {
  const header = template?.header;
  const image = template?.image;
  const body = template?.body;

  // 입력 필드 상태
  const [inputValue, setInputValue] = useState<number>(inputField?.defaultValue || 0);
  const [packageUnit, setPackageUnit] = useState<string | undefined>(undefined);
  const [isModalOpen, setIsModalOpen] = useState(false);

  // 담기/취소 토글 상태
  const [isCarted, setIsCarted] = useState(false);

  // 헤더 값 추출
  const codeValue = header?.codeField ? row[header.codeField] : null;
  const titleValue = header?.titleField ? row[header.titleField] : null;

  // 이미지 URL 결정
  const imageUrl =
    image?.enabled && image?.imageColumn && row[image.imageColumn]
      ? String(row[image.imageColumn])
      : image?.defaultImage || DEFAULT_CARD_IMAGE;

  // 계산 필드 값 계산
  const calculatedValue = useMemo(() => {
    if (!calculatedField?.enabled || !calculatedField?.formula) return null;
    return evaluateFormula(calculatedField.formula, row, inputValue);
  }, [calculatedField, row, inputValue]);

  // effectiveMax: DB 컬럼 우선, 없으면 inputField.max 폴백
  const effectiveMax = useMemo(() => {
    if (inputField?.maxColumn) {
      const colVal = Number(row[inputField.maxColumn]);
      if (!isNaN(colVal) && colVal > 0) return colVal;
    }
    return inputField?.max ?? 999999;
  }, [inputField, row]);

  // 기본값이 설정되지 않은 경우 최대값으로 자동 초기화
  useEffect(() => {
    if (inputField?.enabled && !inputField?.defaultValue && effectiveMax > 0 && effectiveMax < 999999) {
      setInputValue(effectiveMax);
    }
  }, [effectiveMax, inputField?.enabled, inputField?.defaultValue]);

  const cardStyle: React.CSSProperties = {
    height: `${scaled.cardHeight}px`,
    overflow: "hidden",
  };

  const headerStyle: React.CSSProperties = {
    padding: `${scaled.headerPaddingY}px ${scaled.headerPaddingX}px`,
  };

  const bodyStyle: React.CSSProperties = {
    padding: `${scaled.padding}px`,
    gap: `${scaled.gap}px`,
  };

  const imageContainerStyle: React.CSSProperties = {
    width: `${scaled.imageSize}px`,
    height: `${scaled.imageSize}px`,
  };

  const handleInputClick = (e: React.MouseEvent) => {
    e.stopPropagation();
    setIsModalOpen(true);
  };

  const handleInputConfirm = (value: number, unit?: string) => {
    setInputValue(value);
    setPackageUnit(unit);
  };

  // 담기: sharedData에 CartItem 누적 + 이벤트 발행 + 토글
  const handleCartAdd = () => {
    const cartItem: CartItem = {
      row,
      quantity: inputValue,
      packageUnit: packageUnit || undefined,
    };

    const existing = getSharedData<CartItem[]>("cart_items") || [];
    setSharedData("cart_items", [...existing, cartItem]);
    publish("cart_item_added", cartItem);

    setIsCarted(true);
    toast.success("장바구니에 담겼습니다.");

    if (cartAction?.navigateMode === "screen" && cartAction.targetScreenId) {
      router.push(`/pop/screens/${cartAction.targetScreenId}`);
    }
  };

  // 취소: sharedData에서 해당 아이템 제거 + 이벤트 발행 + 토글 복원
  const handleCartCancel = () => {
    const existing = getSharedData<CartItem[]>("cart_items") || [];
    const rowKey = JSON.stringify(row);
    const filtered = existing.filter((item) => JSON.stringify(item.row) !== rowKey);
    setSharedData("cart_items", filtered);
    publish("cart_item_removed", { row });

    setIsCarted(false);
    toast.info("장바구니에서 제거되었습니다.");
  };

  // pop-icon 스타일 담기/취소 버튼 아이콘 크기 (스케일 반영)
  const iconSize = Math.max(14, Math.round(18 * (scaled.bodyTextSize / 11)));
  const cartLabel = cartAction?.label || "담기";
  const cancelLabel = cartAction?.cancelLabel || "취소";

  return (
    <div
      className="bg-card rounded-lg border shadow-sm transition-all duration-150 hover:border-2 hover:border-blue-500 hover:shadow-md"
      style={cardStyle}
    >
      {/* 헤더 영역 */}
      {(codeValue !== null || titleValue !== null) && (
        <div className="bg-muted/30 border-b" style={headerStyle}>
          <div className="flex items-center gap-2">
            {codeValue !== null && (
              <span
                className="text-muted-foreground shrink-0 font-medium"
                style={{ fontSize: `${scaled.codeTextSize}px` }}
              >
                {formatValue(codeValue)}
              </span>
            )}
            {titleValue !== null && (
              <MarqueeText className="text-foreground font-bold" style={{ fontSize: `${scaled.titleTextSize}px` }}>
                {formatValue(titleValue)}
              </MarqueeText>
            )}
          </div>
        </div>
      )}

      {/* 본문 영역 */}
      <div className="flex" style={bodyStyle}>
        {/* 이미지 (왼쪽) */}
        {image?.enabled && (
          <div className="shrink-0">
            <div
              className="bg-muted/30 flex items-center justify-center overflow-hidden rounded-md border"
              style={imageContainerStyle}
            >
              <img
                src={imageUrl}
                alt=""
                className="h-full w-full object-contain p-1"
                onError={(e) => {
                  const target = e.target as HTMLImageElement;
                  if (target.src !== DEFAULT_CARD_IMAGE) target.src = DEFAULT_CARD_IMAGE;
                }}
              />
            </div>
          </div>
        )}

        {/* 필드 목록 (중간, flex-1) */}
        <div className="min-w-0 flex-1">
          <div style={{ display: "flex", flexDirection: "column", gap: `${Math.round(scaled.gap / 2)}px` }}>
            {body?.fields && body.fields.length > 0 ? (
              body.fields.map((field) => <FieldRow key={field.id} field={field} row={row} scaled={scaled} />)
            ) : (
              <div
                className="text-muted-foreground flex items-center justify-center"
                style={{ fontSize: `${scaled.bodyTextSize}px` }}
              >
                본문 필드를 추가하세요
              </div>
            )}

            {/* 계산 필드 */}
            {calculatedField?.enabled && calculatedValue !== null && (
              <div
                className="flex items-baseline"
                style={{ gap: `${Math.round(scaled.gap / 2)}px`, fontSize: `${scaled.bodyTextSize}px` }}
              >
                <span
                  className="text-muted-foreground shrink-0"
                  style={{ minWidth: `${Math.round(50 * (scaled.bodyTextSize / 12))}px` }}
                >
                  {calculatedField.label || "계산값"}
                </span>
                <MarqueeText className="font-medium text-orange-600">
                  {calculatedValue.toLocaleString()}
                  {calculatedField.unit ? ` ${calculatedField.unit}` : ""}
                </MarqueeText>
              </div>
            )}
          </div>
        </div>

        {/* 오른쪽: 수량 버튼 + 담기/취소 버튼 (inputField 활성화 시만) */}
        {inputField?.enabled && (
          <div className="ml-2 flex shrink-0 flex-col items-stretch justify-center gap-2" style={{ minWidth: "100px" }}>
            {/* 수량 버튼 */}
            <button
              type="button"
              onClick={handleInputClick}
              className="hover:border-primary rounded-lg border-2 border-gray-300 bg-white px-2 py-1.5 text-center active:bg-gray-50"
            >
              <span className="block text-lg leading-tight font-bold">{inputValue.toLocaleString()}</span>
              <span className="text-muted-foreground block text-[12px]">{inputField.unit || "EA"}</span>
            </button>

            {/* pop-icon 스타일 담기/취소 토글 버튼 */}
            {isCarted ? (
              <button
                type="button"
                onClick={handleCartCancel}
                className="bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80 flex flex-col items-center justify-center gap-0.5 rounded-xl px-2 py-1.5 transition-colors duration-150"
              >
                <X size={iconSize} />
                <span
                  style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }}
                  className="leading-tight font-semibold"
                >
                  {cancelLabel}
                </span>
              </button>
            ) : (
              <button
                type="button"
                onClick={handleCartAdd}
                className="flex flex-col items-center justify-center gap-0.5 rounded-xl bg-gradient-to-br from-amber-400 to-orange-500 px-2 py-1.5 text-white transition-colors duration-150 hover:from-amber-500 hover:to-orange-600 active:from-amber-600 active:to-orange-700"
              >
                {cartAction?.iconType === "emoji" && cartAction?.iconValue ? (
                  <span style={{ fontSize: `${iconSize}px` }}>{cartAction.iconValue}</span>
                ) : (
                  <DynamicLucideIcon name={cartAction?.iconValue} size={iconSize} />
                )}
                <span
                  style={{ fontSize: `${Math.max(9, scaled.bodyTextSize - 2)}px` }}
                  className="leading-tight font-semibold"
                >
                  {cartLabel}
                </span>
              </button>
            )}
          </div>
        )}
      </div>

      {/* 숫자 입력 모달 */}
      {inputField?.enabled && (
        <NumberInputModal
          open={isModalOpen}
          onOpenChange={setIsModalOpen}
          unit={inputField.unit || "EA"}
          initialValue={inputValue}
          initialPackageUnit={packageUnit}
          min={inputField.min || 0}
          maxValue={effectiveMax}
          onConfirm={handleInputConfirm}
        />
      )}
    </div>
  );
}

// ===== 필드 행 컴포넌트 =====

function FieldRow({ field, row, scaled }: { field: CardFieldBinding; row: RowData; scaled: ScaledConfig }) {
  const value = row[field.columnName];

  // 비율 기반 라벨 최소 너비
  const labelMinWidth = Math.round(50 * (scaled.bodyTextSize / 12));

  return (
    <div
      className="flex items-baseline"
      style={{ gap: `${Math.round(scaled.gap / 2)}px`, fontSize: `${scaled.bodyTextSize}px` }}
    >
      {/* 라벨 */}
      <span className="text-muted-foreground shrink-0" style={{ minWidth: `${labelMinWidth}px` }}>
        {field.label}
      </span>
      {/* 값 - 기본 검정색, textColor 설정 시 해당 색상 */}
      <MarqueeText className="font-medium" style={{ color: field.textColor || "#000000" }}>
        {formatValue(value)}
      </MarqueeText>
    </div>
  );
}

// ===== 값 포맷팅 =====

function formatValue(value: unknown): string {
  if (value === null || value === undefined) {
    return "-";
  }
  if (typeof value === "number") {
    return value.toLocaleString();
  }
  if (typeof value === "boolean") {
    return value ? "예" : "아니오";
  }
  if (value instanceof Date) {
    return value.toLocaleDateString();
  }
  // ISO 날짜 문자열 감지 및 포맷
  if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value)) {
    const date = new Date(value);
    if (!isNaN(date.getTime())) {
      // MM-DD 형식으로 표시
      return `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
    }
  }
  return String(value);
}

// ===== 계산식 평가 =====

/**
 * 간단한 계산식을 평가합니다.
 * 지원 연산: +, -, *, /
 * 특수 변수: $input (입력 필드 값)
 *
 * @param formula 계산식 (예: "order_qty - inbound_qty", "$input - received_qty")
 * @param row 데이터 행
 * @param inputValue 입력 필드 값
 * @returns 계산 결과 또는 null (계산 실패 시)
 */
function evaluateFormula(formula: string, row: RowData, inputValue: number): number | null {
  try {
    // 수식에서 컬럼명과 $input을 실제 값으로 치환
    let expression = formula;

    // $input을 입력값으로 치환
    expression = expression.replace(/\$input/g, String(inputValue));

    // 컬럼명을 값으로 치환 (알파벳, 숫자, 언더스코어로 구성된 식별자)
    const columnPattern = /[a-zA-Z_][a-zA-Z0-9_]*/g;
    expression = expression.replace(columnPattern, (match) => {
      // 이미 숫자로 치환된 경우 스킵
      if (/^\d+$/.test(match)) return match;

      const value = row[match];
      if (value === null || value === undefined) return "0";
      if (typeof value === "number") return String(value);
      const parsed = parseFloat(String(value));
      return isNaN(parsed) ? "0" : String(parsed);
    });

    // 안전한 계산 (기본 산술 연산만 허용)
    // 허용: 숫자, +, -, *, /, (, ), 공백, 소수점
    if (!/^[\d\s+\-*/().]+$/.test(expression)) {
      console.warn("Invalid formula expression:", expression);
      return null;
    }

    // eval 대신 Function 사용 (더 안전)
    const result = new Function(`return (${expression})`)();

    if (typeof result !== "number" || isNaN(result) || !isFinite(result)) {
      return null;
    }

    return Math.round(result * 100) / 100; // 소수점 2자리까지
  } catch (error) {
    console.warn("Formula evaluation error:", error);
    return null;
  }
}
