"use client";

import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";

/**
 * V2Select
 *
 * 통합 선택 컴포넌트
 * - dropdown: 드롭다운 선택
 * - radio: 라디오 버튼 그룹
 * - check: 체크박스 그룹
 * - tag: 태그 선택
 * - toggle: 토글 스위치
 * - swap: 스왑 선택 (좌우 이동)
 */

import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { V2SelectProps, SelectOption, V2SelectFilter } from "@/types/v2-components";
import { Check, ChevronsUpDown, X, ArrowLeftRight } from "lucide-react";
import { apiClient } from "@/lib/api/client";
import V2FormContext from "./V2FormContext";

/**
 * 드롭다운 선택 컴포넌트
 */
const DropdownSelect = forwardRef<
  HTMLButtonElement,
  {
    options: SelectOption[];
    value?: string | string[];
    onChange?: (value: string | string[]) => void;
    placeholder?: string;
    searchable?: boolean;
    multiple?: boolean;
    maxSelect?: number;
    allowClear?: boolean;
    disabled?: boolean;
    className?: string;
    style?: React.CSSProperties;
  }
>(
  (
    {
      options,
      value,
      onChange,
      placeholder = "선택",
      searchable,
      multiple,
      maxSelect,
      allowClear = true,
      disabled,
      className,
      style,
    },
    ref,
  ) => {
    const [open, setOpen] = useState(false);
    const textRef = useRef<HTMLSpanElement>(null);
    const [isTruncated, setIsTruncated] = useState(false);
    const [hoverTooltip, setHoverTooltip] = useState(false);

    // 현재 선택된 값 존재 여부
    const hasValue = useMemo(() => {
      if (!value) return false;
      if (Array.isArray(value)) return value.length > 0;
      return value !== "";
    }, [value]);

    // 단일 선택 + 검색 불가능 → 기본 Select 사용
    if (!searchable && !multiple) {
      return (
        <div className="group relative w-full">
          <Select
            value={typeof value === "string" ? value : (value?.[0] ?? "")}
            onValueChange={(v) => onChange?.(v)}
            disabled={disabled}
          >
            {/* SelectTrigger에 style로 직접 height 전달 (Radix Select.Root는 DOM 없어서 h-full 체인 끊김) */}
            <SelectTrigger
              ref={ref}
              className={cn("w-full", allowClear && hasValue ? "pr-8" : "", className)}
              style={style}
            >
              <SelectValue placeholder={placeholder} />
            </SelectTrigger>
            <SelectContent>
              {options
                .filter((option) => option.value != null && option.value !== "")
                .map((option) => (
                  <SelectItem key={option.value} value={option.value}>
                    {option.label}
                  </SelectItem>
                ))}
            </SelectContent>
          </Select>
          {/* 초기화 버튼 (값이 있을 때만 표시) */}
          {allowClear && hasValue && !disabled && (
            <span
              role="button"
              tabIndex={-1}
              className="absolute top-1/2 right-7 z-10 -translate-y-1/2 cursor-pointer"
              onClick={(e) => {
                e.stopPropagation();
                e.preventDefault();
                onChange?.("");
              }}
              onPointerDown={(e) => {
                e.stopPropagation();
                e.preventDefault();
              }}
            >
              <X className="h-3.5 w-3.5 opacity-40 transition-opacity hover:opacity-100" />
            </span>
          )}
        </div>
      );
    }

    // 검색 가능 또는 다중 선택 → Combobox 사용
    // null/undefined value를 가진 옵션 필터링 (cmdk가 value={null}일 때 크래시 발생)
    const safeOptions = useMemo(() => options.filter((o) => o.value != null && o.value !== ""), [options]);

    const selectedValues = useMemo(() => {
      if (!value) return [];
      return Array.isArray(value) ? value : [value];
    }, [value]);

    const selectedLabels = useMemo(() => {
      return safeOptions
        .filter((o) => selectedValues.includes(o.value))
        .map((o) => o.label)
        .filter(Boolean) as string[];
    }, [selectedValues, safeOptions]);

    useEffect(() => {
      const el = textRef.current;
      if (el) {
        setIsTruncated(el.scrollWidth > el.clientWidth);
      }
    }, [selectedLabels]);

    const handleSelect = useCallback(
      (selectedValue: string) => {
        if (multiple) {
          const newValues = selectedValues.includes(selectedValue)
            ? selectedValues.filter((v) => v !== selectedValue)
            : maxSelect && selectedValues.length >= maxSelect
              ? selectedValues
              : [...selectedValues, selectedValue];
          onChange?.(newValues);
        } else {
          onChange?.(selectedValue);
          setOpen(false);
        }
      },
      [multiple, selectedValues, maxSelect, onChange],
    );

    const handleClear = useCallback(
      (e: React.MouseEvent) => {
        e.stopPropagation();
        onChange?.(multiple ? [] : "");
      },
      [multiple, onChange],
    );

    const displayText =
      selectedLabels.length > 0 ? (multiple ? selectedLabels.join(", ") : selectedLabels[0]) : placeholder;
    const isPlaceholder = selectedLabels.length === 0;

    return (
      <div
        className="relative w-full"
        onMouseEnter={() => {
          if (isTruncated && multiple) setHoverTooltip(true);
        }}
        onMouseLeave={() => setHoverTooltip(false)}
      >
        <Popover
          open={open}
          onOpenChange={(isOpen) => {
            setOpen(isOpen);
            if (isOpen) setHoverTooltip(false);
          }}
        >
          <PopoverTrigger asChild>
            <Button
              ref={ref}
              variant="outline"
              role="combobox"
              aria-expanded={open}
              disabled={disabled}
              className={cn(
                "w-full justify-between font-normal",
                "bg-transparent hover:bg-transparent",
                "border-input shadow-xs",
                "h-6 px-2 py-0 text-sm",
                className,
              )}
              style={style}
            >
              <span
                ref={textRef}
                className="flex-1 truncate text-left"
                {...(isPlaceholder ? { "data-placeholder": placeholder } : {})}
              >
                {displayText}
              </span>
              <div className="ml-2 flex items-center gap-1">
                {allowClear && selectedValues.length > 0 && (
                  <span
                    role="button"
                    tabIndex={-1}
                    onClick={handleClear}
                    onPointerDown={(e) => {
                      e.stopPropagation();
                      e.preventDefault();
                    }}
                  >
                    <X className="h-4 w-4 opacity-50 hover:opacity-100" />
                  </span>
                )}
                <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
              </div>
            </Button>
          </PopoverTrigger>
          <PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
            <Command
              filter={(itemValue, search) => {
                if (!search) return 1;
                const option = safeOptions.find((o) => o.value === itemValue);
                const label = (option?.label || option?.value || "").toLowerCase();
                if (label.includes(search.toLowerCase())) return 1;
                return 0;
              }}
            >
              {searchable && <CommandInput placeholder="검색..." className="h-9" />}
              <CommandList>
                <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
                <CommandGroup>
                  {safeOptions.map((option) => {
                    const displayLabel = option.label || option.value || "(빈 값)";
                    return (
                      <CommandItem key={option.value} value={option.value} onSelect={() => handleSelect(option.value)}>
                        <Check
                          className={cn(
                            "mr-2 h-4 w-4",
                            selectedValues.includes(option.value) ? "opacity-100" : "opacity-0",
                          )}
                        />
                        {displayLabel}
                      </CommandItem>
                    );
                  })}
                </CommandGroup>
              </CommandList>
            </Command>
          </PopoverContent>
        </Popover>
        {hoverTooltip && !open && (
          <div className="bg-popover animate-in fade-in-0 zoom-in-95 absolute bottom-full left-0 z-50 mb-1 rounded-md border px-3 py-1.5 shadow-md">
            <div className="space-y-0.5 text-xs">
              {selectedLabels.map((label, i) => (
                <div key={i}>{label}</div>
              ))}
            </div>
          </div>
        )}
      </div>
    );
  },
);
DropdownSelect.displayName = "DropdownSelect";

/**
 * 라디오 선택 컴포넌트
 */
const RadioSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string;
    onChange?: (value: string) => void;
    disabled?: boolean;
    className?: string;
  }
>(({ options, value, onChange, disabled, className }, ref) => {
  return (
    <RadioGroup
      ref={ref}
      value={value ?? ""}
      onValueChange={onChange}
      disabled={disabled}
      className={cn("flex flex-wrap gap-4", className)}
    >
      {options.map((option) => (
        <div key={option.value} className="flex items-center space-x-2">
          <RadioGroupItem value={option.value} id={`radio-${option.value}`} />
          <Label htmlFor={`radio-${option.value}`} className="cursor-pointer text-sm">
            {option.label}
          </Label>
        </div>
      ))}
    </RadioGroup>
  );
});
RadioSelect.displayName = "RadioSelect";

/**
 * 체크박스 선택 컴포넌트
 */
const CheckSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string[];
    onChange?: (value: string[]) => void;
    maxSelect?: number;
    disabled?: boolean;
    className?: string;
  }
>(({ options, value = [], onChange, maxSelect, disabled, className }, ref) => {
  const handleChange = useCallback(
    (optionValue: string, checked: boolean) => {
      if (checked) {
        if (maxSelect && value.length >= maxSelect) return;
        onChange?.([...value, optionValue]);
      } else {
        onChange?.(value.filter((v) => v !== optionValue));
      }
    },
    [value, maxSelect, onChange],
  );

  return (
    <div ref={ref} className={cn("flex flex-wrap gap-4", className)}>
      {options.map((option) => (
        <div key={option.value} className="flex items-center space-x-2">
          <Checkbox
            id={`check-${option.value}`}
            checked={value.includes(option.value)}
            onCheckedChange={(checked) => handleChange(option.value, checked as boolean)}
            disabled={disabled || (maxSelect && value.length >= maxSelect && !value.includes(option.value))}
          />
          <Label htmlFor={`check-${option.value}`} className="cursor-pointer text-sm">
            {option.label}
          </Label>
        </div>
      ))}
    </div>
  );
});
CheckSelect.displayName = "CheckSelect";

/**
 * 태그 선택 컴포넌트
 */
const TagSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string[];
    onChange?: (value: string[]) => void;
    maxSelect?: number;
    disabled?: boolean;
    className?: string;
  }
>(({ options, value = [], onChange, maxSelect, disabled, className }, ref) => {
  const handleToggle = useCallback(
    (optionValue: string) => {
      const isSelected = value.includes(optionValue);
      if (isSelected) {
        onChange?.(value.filter((v) => v !== optionValue));
      } else {
        if (maxSelect && value.length >= maxSelect) return;
        onChange?.([...value, optionValue]);
      }
    },
    [value, maxSelect, onChange],
  );

  return (
    <div ref={ref} className={cn("flex flex-wrap gap-2", className)}>
      {options.map((option) => {
        const isSelected = value.includes(option.value);
        return (
          <Badge
            key={option.value}
            variant={isSelected ? "default" : "outline"}
            className={cn("cursor-pointer transition-colors", disabled && "cursor-not-allowed opacity-50")}
            onClick={() => !disabled && handleToggle(option.value)}
          >
            {option.label}
            {isSelected && <X className="ml-1 h-3 w-3" />}
          </Badge>
        );
      })}
    </div>
  );
});
TagSelect.displayName = "TagSelect";

/**
 * 태그박스 선택 컴포넌트 (태그 형태 + 체크박스 드롭다운)
 * - 선택된 값들이 태그(Badge)로 표시됨
 * - 클릭하면 체크박스 목록이 드롭다운으로 열림
 */
const TagboxSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string[];
    onChange?: (value: string[]) => void;
    placeholder?: string;
    maxSelect?: number;
    disabled?: boolean;
    className?: string;
    style?: React.CSSProperties;
  }
>(({ options, value = [], onChange, placeholder = "선택하세요", maxSelect, disabled, className, style }, ref) => {
  const [open, setOpen] = useState(false);

  // 선택된 옵션들의 라벨 가져오기
  const selectedOptions = useMemo(() => options.filter((o) => value.includes(o.value)), [options, value]);

  // 체크박스 토글 핸들러
  const handleToggle = useCallback(
    (optionValue: string) => {
      const isSelected = value.includes(optionValue);
      if (isSelected) {
        onChange?.(value.filter((v) => v !== optionValue));
      } else {
        if (maxSelect && value.length >= maxSelect) return;
        onChange?.([...value, optionValue]);
      }
    },
    [value, maxSelect, onChange],
  );

  // 태그 제거 핸들러
  const handleRemove = useCallback(
    (e: React.MouseEvent, optionValue: string) => {
      e.stopPropagation();
      onChange?.(value.filter((v) => v !== optionValue));
    },
    [value, onChange],
  );

  // 🔧 높이 처리: style.height가 있으면 minHeight로 사용 (기본 40px 보장)
  const triggerStyle: React.CSSProperties = {
    minHeight: style?.height || 40,
    height: style?.height || "auto",
    maxWidth: "100%", // 🔧 부모 컨테이너를 넘지 않도록
  };

  return (
    <div ref={ref} className={cn("w-full max-w-full overflow-hidden", className)} style={{ width: style?.width }}>
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <div
            className={cn(
              "border-input bg-background ring-offset-background flex w-full max-w-full cursor-pointer flex-wrap items-center gap-1.5 overflow-hidden rounded-md border px-3 py-2 text-sm",
              "hover:border-primary/50 focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
              disabled && "cursor-not-allowed opacity-50",
            )}
            style={triggerStyle}
          >
            {selectedOptions.length > 0 ? (
              <>
                {selectedOptions.map((option) => (
                  <Badge key={option.value} variant="secondary" className="flex items-center gap-1 px-2 py-0.5">
                    {option.label}
                    <X
                      className="hover:text-destructive h-3 w-3 cursor-pointer"
                      onClick={(e) => !disabled && handleRemove(e, option.value)}
                    />
                  </Badge>
                ))}
              </>
            ) : (
              <span className="text-muted-foreground">{placeholder}</span>
            )}
            <ChevronsUpDown className="ml-auto h-4 w-4 shrink-0 opacity-50" />
          </div>
        </PopoverTrigger>
        <PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
          <div className="max-h-[300px] overflow-auto p-2">
            {options.map((option) => {
              const isSelected = value.includes(option.value);
              return (
                <div
                  key={option.value}
                  className={cn(
                    "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm",
                    "hover:bg-accent hover:text-accent-foreground",
                    isSelected && "bg-accent/50",
                  )}
                  onClick={() => !disabled && handleToggle(option.value)}
                >
                  <Checkbox checked={isSelected} disabled={disabled} className="pointer-events-none" />
                  <span>{option.label}</span>
                </div>
              );
            })}
            {options.length === 0 && (
              <div className="text-muted-foreground py-2 text-center text-sm">옵션이 없습니다</div>
            )}
          </div>
        </PopoverContent>
      </Popover>
    </div>
  );
});
TagboxSelect.displayName = "TagboxSelect";

/**
 * 토글 선택 컴포넌트 (Boolean용)
 */
const ToggleSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string;
    onChange?: (value: string) => void;
    disabled?: boolean;
    className?: string;
  }
>(({ options, value, onChange, disabled, className }, ref) => {
  // 토글은 2개 옵션만 지원
  const [offOption, onOption] =
    options.length >= 2
      ? [options[0], options[1]]
      : [
          { value: "false", label: "아니오" },
          { value: "true", label: "예" },
        ];

  const isOn = value === onOption.value;

  return (
    <div ref={ref} className={cn("flex items-center gap-3", className)}>
      <span className={cn("text-sm", !isOn && "font-medium")}>{offOption.label}</span>
      <Switch
        checked={isOn}
        onCheckedChange={(checked) => onChange?.(checked ? onOption.value : offOption.value)}
        disabled={disabled}
      />
      <span className={cn("text-sm", isOn && "font-medium")}>{onOption.label}</span>
    </div>
  );
});
ToggleSelect.displayName = "ToggleSelect";

/**
 * 스왑 선택 컴포넌트 (좌우 이동 방식)
 */
const SwapSelect = forwardRef<
  HTMLDivElement,
  {
    options: SelectOption[];
    value?: string[];
    onChange?: (value: string[]) => void;
    maxSelect?: number;
    disabled?: boolean;
    className?: string;
  }
>(({ options, value = [], onChange, disabled, className }, ref) => {
  const available = useMemo(() => options.filter((o) => !value.includes(o.value)), [options, value]);

  const selected = useMemo(() => options.filter((o) => value.includes(o.value)), [options, value]);

  const handleMoveRight = useCallback(
    (optionValue: string) => {
      onChange?.([...value, optionValue]);
    },
    [value, onChange],
  );

  const handleMoveLeft = useCallback(
    (optionValue: string) => {
      onChange?.(value.filter((v) => v !== optionValue));
    },
    [value, onChange],
  );

  const handleMoveAllRight = useCallback(() => {
    onChange?.(options.map((o) => o.value));
  }, [options, onChange]);

  const handleMoveAllLeft = useCallback(() => {
    onChange?.([]);
  }, [onChange]);

  return (
    <div ref={ref} className={cn("flex items-stretch gap-2", className)}>
      {/* 왼쪽: 선택 가능 */}
      <div className="flex min-h-0 flex-1 flex-col rounded-md border">
        <div className="bg-muted shrink-0 border-b p-2 text-xs font-medium">선택 가능</div>
        <div className="min-h-0 flex-1 space-y-1 overflow-y-auto p-2">
          {available.map((option) => (
            <div
              key={option.value}
              className={cn(
                "hover:bg-accent cursor-pointer rounded p-2 text-sm",
                disabled && "cursor-not-allowed opacity-50",
              )}
              onClick={() => !disabled && handleMoveRight(option.value)}
            >
              {option.label}
            </div>
          ))}
          {available.length === 0 && <div className="text-muted-foreground p-2 text-xs">항목 없음</div>}
        </div>
      </div>

      {/* 중앙: 이동 버튼 */}
      <div className="flex flex-col justify-center gap-1">
        <Button
          variant="outline"
          size="icon"
          className="h-8 w-8"
          onClick={handleMoveAllRight}
          disabled={disabled || available.length === 0}
        >
          <ArrowLeftRight className="h-4 w-4 rotate-180" />
        </Button>
        <Button
          variant="outline"
          size="icon"
          className="h-8 w-8"
          onClick={handleMoveAllLeft}
          disabled={disabled || selected.length === 0}
        >
          <ArrowLeftRight className="h-4 w-4" />
        </Button>
      </div>

      {/* 오른쪽: 선택됨 */}
      <div className="flex min-h-0 flex-1 flex-col rounded-md border">
        <div className="bg-primary/10 shrink-0 border-b p-2 text-xs font-medium">선택됨</div>
        <div className="min-h-0 flex-1 space-y-1 overflow-y-auto p-2">
          {selected.map((option) => (
            <div
              key={option.value}
              className={cn(
                "hover:bg-accent flex cursor-pointer items-center justify-between rounded p-2 text-sm",
                disabled && "cursor-not-allowed opacity-50",
              )}
              onClick={() => !disabled && handleMoveLeft(option.value)}
            >
              <span>{option.label}</span>
              <X className="h-3 w-3 opacity-50" />
            </div>
          ))}
          {selected.length === 0 && <div className="text-muted-foreground p-2 text-xs">선택 없음</div>}
        </div>
      </div>
    </div>
  );
});
SwapSelect.displayName = "SwapSelect";

/**
 * 메인 V2Select 컴포넌트
 */
export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>((props, ref) => {
  const {
    id,
    label,
    required,
    readonly,
    disabled,
    style,
    size,
    config: configProp,
    value,
    onChange,
    onFormDataChange,
    tableName,
    columnName,
    isDesignMode, // 🔧 디자인 모드 (클릭 방지)
  } = props;

  // config가 없으면 기본값 사용
  const config = configProp || { mode: "dropdown" as const, source: "static" as const, options: [] };

  // 엔티티 자동 채움: 같은 폼의 다른 컴포넌트 중 참조 테이블 컬럼을 자동 감지
  const allComponents = (props as any).allComponents as any[] | undefined;

  const [options, setOptions] = useState<SelectOption[]>(config.options || []);
  const [loading, setLoading] = useState(false);
  const [optionsLoaded, setOptionsLoaded] = useState(false);

  // 옵션 로딩에 필요한 값들만 추출 (객체 참조 대신 원시값 사용)
  const rawSource = config.source;
  const categoryTable = (config as any).categoryTable;
  const categoryColumn = (config as any).categoryColumn;

  // category 소스 유지 (category_values 테이블에서 로드)
  const source = rawSource;
  const codeGroup = config.codeGroup;

  const entityTable = config.entityTable;
  const entityValueColumn = config.entityValueColumn || config.entityValueField;
  const entityLabelColumn = config.entityLabelColumn || config.entityLabelField;
  const table = config.table;
  const valueColumn = config.valueColumn;
  const labelColumn = config.labelColumn;
  const apiEndpoint = config.apiEndpoint;
  const staticOptions = config.options;
  const configFilters = config.filters;

  // 계층 코드 연쇄 선택 관련
  const hierarchical = config.hierarchical;
  const parentField = config.parentField;

  // FormContext에서 부모 필드 값 가져오기 (Context가 없으면 null)
  const formContext = useContext(V2FormContext);

  /**
   * 필터 조건을 API 전달용 JSON으로 변환
   * field/user 타입은 런타임 값으로 치환
   */
  const resolvedFiltersJson = useMemo(() => {
    if (!configFilters || configFilters.length === 0) return undefined;

    const resolved: Array<{ column: string; operator: string; value: unknown }> = [];

    for (const f of configFilters) {
      const vt = f.valueType || "static";

      // isNull/isNotNull은 값 불필요
      if (f.operator === "isNull" || f.operator === "isNotNull") {
        resolved.push({ column: f.column, operator: f.operator, value: null });
        continue;
      }

      let resolvedValue: unknown = f.value;

      if (vt === "field" && f.fieldRef) {
        // 다른 폼 필드 참조
        if (formContext) {
          resolvedValue = formContext.getValue(f.fieldRef);
        } else {
          const fd = (props as any).formData;
          resolvedValue = fd?.[f.fieldRef];
        }
        // 참조 필드 값이 비어있으면 이 필터 건너뜀
        if (resolvedValue === undefined || resolvedValue === null || resolvedValue === "") continue;
      } else if (vt === "user" && f.userField) {
        // 로그인 사용자 정보 참조 (props에서 가져옴)
        const userMap: Record<string, string | undefined> = {
          companyCode: (props as any).companyCode,
          userId: (props as any).userId,
          deptCode: (props as any).deptCode,
          userName: (props as any).userName,
        };
        resolvedValue = userMap[f.userField];
        if (!resolvedValue) continue;
      }

      resolved.push({ column: f.column, operator: f.operator, value: resolvedValue });
    }

    return resolved.length > 0 ? JSON.stringify(resolved) : undefined;
  }, [configFilters, formContext, props]);

  // 부모 필드의 값 계산
  const parentValue = useMemo(() => {
    if (!hierarchical || !parentField) return null;

    // FormContext가 있으면 거기서 값 가져오기
    if (formContext) {
      const val = formContext.getValue(parentField);
      return val as string | null;
    }

    return null;
  }, [hierarchical, parentField, formContext]);

  // 데이터 소스에 따른 옵션 로딩 (원시값 의존성만 사용)
  useEffect(() => {
    // 계층 구조인 경우 부모 값이 변경되면 다시 로드
    if (hierarchical && source === "code") {
      setOptionsLoaded(false);
    }
  }, [parentValue, hierarchical, source]);

  // 필터 조건이 변경되면 옵션 다시 로드
  useEffect(() => {
    if (resolvedFiltersJson !== undefined) {
      setOptionsLoaded(false);
    }
  }, [resolvedFiltersJson]);

  useEffect(() => {
    // 이미 로드된 경우 스킵 (static 제외, 계층 구조 제외)
    if (optionsLoaded && source !== "static") {
      return;
    }

    const loadOptions = async () => {
      if (source === "static") {
        setOptions(staticOptions || []);
        setOptionsLoaded(true);
        return;
      }

      setLoading(true);
      try {
        let fetchedOptions: SelectOption[] = [];

        if (source === "code" && codeGroup) {
          // 계층 구조 사용 시 자식 코드만 로드
          if (hierarchical) {
            const params = new URLSearchParams();
            if (parentValue) {
              params.append("parentCodeValue", parentValue);
            }
            const queryString = params.toString();
            const url = `/common-codes/categories/${codeGroup}/children${queryString ? `?${queryString}` : ""}`;
            const response = await apiClient.get(url);
            const data = response.data;
            if (data.success && data.data) {
              fetchedOptions = data.data.map((item: { value: string; label: string; hasChildren: boolean }) => ({
                value: item.value,
                label: item.label,
              }));
            }
          } else {
            // 일반 공통코드에서 로드 (올바른 API 경로: /common-codes/categories/:categoryCode/options)
            const response = await apiClient.get(`/common-codes/categories/${codeGroup}/options`);
            const data = response.data;
            if (data.success && data.data) {
              fetchedOptions = data.data.map((item: { value: string; label: string }) => ({
                value: item.value,
                label: item.label,
              }));
            }
          }
        } else if (source === "db" && table) {
          // DB 테이블에서 로드
          const dbParams: Record<string, any> = {
            value: valueColumn || "id",
            label: labelColumn || "name",
          };
          if (resolvedFiltersJson) dbParams.filters = resolvedFiltersJson;
          const response = await apiClient.get(`/entity/${table}/options`, {
            params: dbParams,
          });
          const data = response.data;
          if (data.success && data.data) {
            fetchedOptions = data.data;
          }
        } else if (source === "entity" && entityTable) {
          // 엔티티(참조 테이블)에서 로드
          const valueCol = entityValueColumn || "id";
          const labelCol = entityLabelColumn || "name";
          const entityParams: Record<string, any> = { value: valueCol, label: labelCol };
          if (resolvedFiltersJson) entityParams.filters = resolvedFiltersJson;
          const response = await apiClient.get(`/entity/${entityTable}/options`, {
            params: entityParams,
          });
          const data = response.data;
          if (data.success && data.data) {
            fetchedOptions = data.data;
          }
        } else if (source === "api" && apiEndpoint) {
          // 외부 API에서 로드
          const response = await apiClient.get(apiEndpoint);
          const data = response.data;
          if (Array.isArray(data)) {
            fetchedOptions = data;
          }
        } else if (source === "category") {
          // 카테고리에서 로드 (category_values 테이블)
          // tableName, columnName은 props에서 가져옴
          const catTable = categoryTable || tableName;
          const catColumn = categoryColumn || columnName;

          if (catTable && catColumn) {
            const response = await apiClient.get(`/table-categories/${catTable}/${catColumn}/values`);
            const data = response.data;
            if (data.success && data.data) {
              // 트리 구조를 평탄화하여 옵션으로 변환
              // 🔧 value로 valueCode를 사용 (커스텀 테이블 저장/조회 호환)
              const flattenTree = (
                items: { valueId: number; valueCode: string; valueLabel: string; children?: any[] }[],
                depth: number = 0,
              ): SelectOption[] => {
                const result: SelectOption[] = [];
                for (const item of items) {
                  const prefix = depth > 0 ? "\u00A0\u00A0\u00A0".repeat(depth) + "└ " : "";
                  result.push({
                    value: item.valueCode, // 🔧 valueCode를 value로 사용
                    label: prefix + item.valueLabel,
                  });
                  if (item.children && item.children.length > 0) {
                    result.push(...flattenTree(item.children, depth + 1));
                  }
                }
                return result;
              };
              fetchedOptions = flattenTree(data.data);
            }
          }
        } else if (source === "select" || source === "distinct") {
          // 해당 테이블의 해당 컬럼에서 DISTINCT 값 조회
          const isValidColumnName = columnName && !columnName.startsWith("comp_");
          if (tableName && isValidColumnName) {
            const distinctParams: Record<string, any> = {};
            if (resolvedFiltersJson) distinctParams.filters = resolvedFiltersJson;
            const response = await apiClient.get(`/entity/${tableName}/distinct/${columnName}`, {
              params: distinctParams,
            });
            const data = response.data;
            if (data.success && data.data) {
              fetchedOptions = data.data.map((item: { value: string; label: string }) => ({
                value: String(item.value),
                label: String(item.label),
              }));
            }
          } else if (!isValidColumnName) {
            // columnName이 없거나 유효하지 않으면 빈 옵션
          }
        }

        // null/undefined value 필터링 (cmdk 크래시 방지)
        const sanitized = fetchedOptions
          .filter((o) => o.value != null && String(o.value) !== "")
          .map((o) => ({ ...o, value: String(o.value), label: o.label || String(o.value) }));
        setOptions(sanitized);
        setOptionsLoaded(true);
      } catch (error) {
        console.error("옵션 로딩 실패:", error);
        setOptions([]);
      } finally {
        setLoading(false);
      }
    };

    loadOptions();
  }, [
    source,
    entityTable,
    entityValueColumn,
    entityLabelColumn,
    codeGroup,
    table,
    valueColumn,
    labelColumn,
    apiEndpoint,
    staticOptions,
    optionsLoaded,
    hierarchical,
    parentValue,
    resolvedFiltersJson,
  ]);

  // 레거시 평문값 → 카테고리 코드 자동 정규화 (한글 텍스트로 저장된 데이터 대응)
  const resolvedValue = useMemo(() => {
    if (!value || options.length === 0) return value;

    const resolveOne = (v: string): string => {
      if (options.some((o) => o.value === v)) return v;
      const trimmed = v.trim();
      const match = options.find((o) => {
        const cleanLabel = o.label.replace(/^[\s└]+/, "").trim();
        return cleanLabel === trimmed;
      });
      return match ? match.value : v;
    };

    if (Array.isArray(value)) {
      const resolved = value.map(resolveOne);
      return resolved.every((v, i) => v === value[i]) ? value : resolved;
    }

    // 콤마 구분 복합값 처리 (e.g., "구매품,판매품,CAT_xxx")
    if (typeof value === "string" && value.includes(",")) {
      const parts = value.split(",");
      const resolved = parts.map((p) => resolveOne(p.trim()));
      const result = resolved.join(",");
      return result === value ? value : result;
    }

    return resolveOne(value);
  }, [value, options]);

  // 정규화 결과가 원본과 다르면 onChange로 자동 업데이트 (저장 시 코드 변환)
  useEffect(() => {
    if (!onChange || options.length === 0 || !value || value === resolvedValue) return;
    onChange(resolvedValue as string | string[]);
  }, [resolvedValue]); // eslint-disable-line react-hooks/exhaustive-deps

  // 같은 폼에서 참조 테이블(entityTable) 컬럼을 사용하는 다른 컴포넌트 자동 감지
  const autoFillTargets = useMemo(() => {
    if (source !== "entity" || !entityTable || !allComponents) return [];

    const targets: Array<{ sourceField: string; targetColumnName: string }> = [];
    for (const comp of allComponents) {
      if (comp.id === id) continue;

      // overrides 구조 지원 (DB에서 로드 시 overrides 안에 데이터가 있음)
      const ov = (comp as any).overrides || {};
      const compColumnName = comp.columnName || ov.columnName || comp.componentConfig?.columnName || "";

      // 방법1: entityJoinTable 속성이 있는 경우
      const joinTable = comp.entityJoinTable || ov.entityJoinTable || comp.componentConfig?.entityJoinTable;
      const joinColumn = comp.entityJoinColumn || ov.entityJoinColumn || comp.componentConfig?.entityJoinColumn;
      if (joinTable === entityTable && joinColumn) {
        targets.push({ sourceField: joinColumn, targetColumnName: compColumnName });
        continue;
      }

      // 방법2: columnName이 "테이블명.컬럼명" 형식인 경우 (예: item_info.unit)
      if (compColumnName.includes(".")) {
        const [prefix, actualColumn] = compColumnName.split(".");
        if (prefix === entityTable && actualColumn) {
          targets.push({ sourceField: actualColumn, targetColumnName: compColumnName });
        }
      }
    }
    return targets;
  }, [source, entityTable, allComponents, id]);

  // 엔티티 autoFill 적용 래퍼
  const handleChangeWithAutoFill = useCallback(
    (newValue: string | string[]) => {
      onChange?.(newValue);

      if (autoFillTargets.length === 0 || !onFormDataChange || !entityTable) return;

      const selectedKey = typeof newValue === "string" ? newValue : newValue[0];
      if (!selectedKey) return;

      const valueCol = entityValueColumn || "id";

      apiClient
        .get(`/table-management/tables/${entityTable}/data-with-joins`, {
          params: {
            page: 1,
            size: 1,
            search: JSON.stringify({ [valueCol]: selectedKey }),
            autoFilter: JSON.stringify({ enabled: true, filterColumn: "company_code", userField: "companyCode" }),
          },
        })
        .then((res) => {
          const responseData = res.data?.data;
          const rows = responseData?.data || responseData?.rows || [];
          if (rows.length > 0) {
            const fullData = rows[0];
            for (const target of autoFillTargets) {
              const sourceValue = fullData[target.sourceField];
              if (sourceValue !== undefined) {
                onFormDataChange(target.targetColumnName, sourceValue);
              }
            }
          }
        })
        .catch((err) => console.error("autoFill 조회 실패:", err));
    },
    [onChange, autoFillTargets, onFormDataChange, entityTable, entityValueColumn],
  );

  // 모드별 컴포넌트 렌더링
  const renderSelect = () => {
    if (loading) {
      return <div className="text-muted-foreground flex h-full items-center text-sm">로딩 중...</div>;
    }

    const isDisabled = disabled || readonly;

    // 명시적 높이가 있을 때만 style 전달, 없으면 undefined (기본 높이 h-6 사용)
    const heightStyle: React.CSSProperties | undefined = componentHeight ? { height: componentHeight } : undefined;

    // 🔧 디자인 모드용: 옵션이 없고 dropdown/combobox가 아닌 모드일 때 source 정보 표시
    const nonDropdownModes = ["radio", "check", "checkbox", "tag", "tagbox", "toggle", "swap"];
    if (options.length === 0 && nonDropdownModes.includes(config.mode || "dropdown")) {
      // 데이터 소스 정보 기반 메시지 생성
      let sourceInfo = "";
      if (source === "static") {
        sourceInfo = "정적 옵션 설정 필요";
      } else if (source === "code") {
        sourceInfo = codeGroup ? `공통코드: ${codeGroup}` : "공통코드 설정 필요";
      } else if (source === "entity") {
        sourceInfo = entityTable ? `엔티티: ${entityTable}` : "엔티티 설정 필요";
      } else if (source === "category") {
        const catInfo = categoryTable || tableName || columnName;
        sourceInfo = catInfo ? `카테고리: ${catInfo}` : "카테고리 설정 필요";
      } else if (source === "db") {
        sourceInfo = table ? `테이블: ${table}` : "테이블 설정 필요";
      } else if (!source || source === "distinct") {
        // distinct 또는 미설정인 경우 - 컬럼명 기반으로 표시
        sourceInfo = columnName ? `컬럼: ${columnName}` : "데이터 소스 설정 필요";
      } else {
        sourceInfo = `소스: ${source}`;
      }

      // 모드 이름 한글화
      const modeNames: Record<string, string> = {
        radio: "라디오",
        check: "체크박스",
        checkbox: "체크박스",
        tag: "태그",
        tagbox: "태그박스",
        toggle: "토글",
        swap: "스왑",
      };
      const modeName = modeNames[config.mode || ""] || config.mode;

      return (
        <div className="text-muted-foreground flex h-full items-center justify-center rounded border border-dashed p-2 text-xs">
          <span className="opacity-70">
            [{modeName}] {sourceInfo}
          </span>
        </div>
      );
    }

    switch (config.mode) {
      case "dropdown":
      case "combobox":
        return (
          <DropdownSelect
            options={options}
            value={resolvedValue}
            onChange={handleChangeWithAutoFill}
            placeholder="선택"
            searchable={config.mode === "combobox" ? true : config.searchable}
            multiple={config.multiple}
            maxSelect={config.maxSelect}
            allowClear={config.allowClear}
            disabled={isDisabled}
            style={heightStyle}
          />
        );

      case "radio":
        return (
          <RadioSelect
            options={options}
            value={typeof resolvedValue === "string" ? resolvedValue : resolvedValue?.[0]}
            onChange={(v) => handleChangeWithAutoFill(v)}
            disabled={isDisabled}
          />
        );

      case "check":
      case "checkbox":
        return (
          <CheckSelect
            options={options}
            value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
            onChange={handleChangeWithAutoFill}
            maxSelect={config.maxSelect}
            disabled={isDisabled}
          />
        );

      case "tag":
        return (
          <TagSelect
            options={options}
            value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
            onChange={handleChangeWithAutoFill}
            maxSelect={config.maxSelect}
            disabled={isDisabled}
          />
        );

      case "tagbox":
        return (
          <TagboxSelect
            options={options}
            value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
            onChange={handleChangeWithAutoFill}
            placeholder={config.placeholder || "선택하세요"}
            maxSelect={config.maxSelect}
            disabled={isDisabled}
            style={heightStyle}
          />
        );

      case "toggle":
        return (
          <ToggleSelect
            options={options}
            value={typeof resolvedValue === "string" ? resolvedValue : resolvedValue?.[0]}
            onChange={(v) => handleChangeWithAutoFill(v)}
            disabled={isDisabled}
          />
        );

      case "swap":
        return (
          <SwapSelect
            options={options}
            value={Array.isArray(resolvedValue) ? resolvedValue : resolvedValue ? [resolvedValue] : []}
            onChange={handleChangeWithAutoFill}
            maxSelect={config.maxSelect}
            disabled={isDisabled}
          />
        );

      default:
        return (
          <DropdownSelect
            options={options}
            value={resolvedValue}
            onChange={handleChangeWithAutoFill}
            disabled={isDisabled}
            style={heightStyle}
          />
        );
    }
  };

  const showLabel = label && style?.labelDisplay !== false;
  const componentWidth = size?.width || style?.width;
  const componentHeight = size?.height || style?.height;

  // 라벨 위치 및 높이 계산
  const labelPos = style?.labelPosition || "top";
  const isHorizLabel = labelPos === "left" || labelPos === "right";
  const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
  const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
  const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
  const labelGapValue = style?.labelGap || "8px";

  // 커스텀 스타일 감지
  const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
  const hasCustomBackground = !!style?.backgroundColor;
  const hasCustomRadius = !!style?.borderRadius;

  const customTextStyle: React.CSSProperties = {};
  if (style?.color) customTextStyle.color = style.color;
  if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
  if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
  if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
  const hasCustomText = Object.keys(customTextStyle).length > 0;

  const labelElement = showLabel ? (
    <Label
      htmlFor={id}
      style={{
        ...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
        ...(labelPos === "bottom"
          ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 }
          : {}),
        fontSize: style?.labelFontSize || "14px",
        color: getAdaptiveLabelColor(style?.labelColor),
        fontWeight: style?.labelFontWeight || "500",
      }}
      className="text-sm font-medium whitespace-nowrap"
    >
      {label}
      {required && <span className="ml-0.5 text-amber-500">*</span>}
    </Label>
  ) : null;

  const selectContent = (
    <div
      className={cn(
        isHorizLabel ? "min-w-0 flex-1" : "h-full w-full",
        hasCustomBorder && "**:data-[slot=select-trigger]:border-0! [&_.border]:border-0! [&_button]:border-0!",
        (hasCustomBorder || hasCustomRadius) &&
          "**:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none! [&_button]:rounded-none!",
        hasCustomBackground && "**:data-[slot=select-trigger]:bg-transparent! [&_button]:bg-transparent!",
      )}
      style={{ ...(hasCustomText ? customTextStyle : {}), ...(isHorizLabel ? { height: "100%" } : {}) }}
    >
      {renderSelect()}
    </div>
  );

  if (isHorizLabel && showLabel) {
    return (
      <div
        ref={ref}
        id={id}
        className={cn(
          "flex flex-col gap-1",
          labelPos === "left" ? "sm:flex-row sm:items-center" : "sm:flex-row-reverse sm:items-center",
          isDesignMode && "pointer-events-none",
        )}
        style={{
          width: componentWidth,
          height: componentHeight,
          gap: labelGapValue,
        }}
      >
        <Label
          htmlFor={id}
          style={{
            fontSize: style?.labelFontSize || "14px",
            color: getAdaptiveLabelColor(style?.labelColor),
            fontWeight: style?.labelFontWeight || "500",
          }}
          className="w-full text-sm font-medium whitespace-nowrap sm:w-[120px] sm:shrink-0"
        >
          {label}
          {required && <span className="ml-0.5 text-amber-500">*</span>}
        </Label>
        <div
          className={cn(
            "w-full min-w-0 flex-1",
            hasCustomBorder && "**:data-[slot=select-trigger]:border-0! [&_.border]:border-0! [&_button]:border-0!",
            (hasCustomBorder || hasCustomRadius) &&
              "**:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none! [&_button]:rounded-none!",
            hasCustomBackground && "**:data-[slot=select-trigger]:bg-transparent! [&_button]:bg-transparent!",
          )}
          style={{ ...(hasCustomText ? customTextStyle : {}) }}
        >
          {renderSelect()}
        </div>
      </div>
    );
  }

  return (
    <div
      ref={ref}
      id={id}
      className={cn("relative", isDesignMode && "pointer-events-none")}
      style={{
        width: componentWidth,
        height: componentHeight,
      }}
    >
      {labelElement}
      {selectContent}
    </div>
  );
});

V2Select.displayName = "V2Select";

export default V2Select;
