'use client';

/**
 * UniversalListPage - 통합 리스트 페이지 컴포넌트
 *
 * 59개 리스트 페이지를 하나의 config 기반 컴포넌트로 통합
 * 기존 기능 100% 유지, 테이블 영역만 공통화
 *
 * 지원 모드:
 * - 서버 사이드 필터링/페이지네이션 (기본)
 * - 클라이언트 사이드 필터링/페이지네이션 (clientSideFiltering: true)
 */

import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { useColumnSettings } from '@/hooks/useColumnSettings';
import { useListSearchState } from '@/hooks/useListSearchState';
import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover';
import { toast } from 'sonner';
import { Download, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import {
  IntegratedListTemplateV2,
  type PaginationConfig,
} from '@/components/templates/IntegratedListTemplateV2';
import { downloadExcel, downloadSelectedExcel, type ExcelColumn } from '@/lib/utils/excel-download';
import type {
  UniversalListPageProps,
  TabOption,
  FilterValues,
} from './types';
import { NON_SORTABLE_KEYS } from './types';

export function UniversalListPage<T>({
  config,
  initialData,
  initialTotalCount,
  externalPagination,
  externalSelection,
  onTabChange,
  onSearchChange,
  onFilterChange: onFilterChangeCallback,
  externalIsLoading,
}: UniversalListPageProps<T>) {
  const router = useRouter();
  const params = useParams();
  const locale = (params.locale as string) || 'ko';
  const { canCreate: permCanCreate, canDelete: permCanDelete, canExport } = usePermission();

  // ===== 검색 상태 보존 (sessionStorage + URL) =====
  const {
    getValue: getSearchStateValue,
    setValue: setSearchStateValue,
  } = useListSearchState({ debounceMs: 500 });
  const restoredSearch = getSearchStateValue('search');

  // ===== 상태 관리 =====
  // 원본 데이터 (클라이언트 사이드 필터링용)
  const [rawData, setRawData] = useState<T[]>(initialData || []);
  // UI 상태
  const [currentPage, setCurrentPage] = useState(1);
  const [isLoading, setIsLoading] = useState(!initialData);
  const [searchValue, setSearchValue] = useState(restoredSearch); // UI 입력용 (즉시 반영, sessionStorage에서 복원)
  const [debouncedSearchValue, setDebouncedSearchValue] = useState(restoredSearch); // API 호출용 (debounced)
  const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
  // 탭이 없으면 IntegratedListTemplateV2의 기본값 'default'와 일치해야 함
  const [activeTab, setActiveTab] = useState(
    config.defaultTab || config.tabs?.[0]?.value || 'default'
  );
  const [filters, setFilters] = useState<Record<string, string | string[]>>(
    config.initialFilters || {}
  );
  const [tabs, setTabs] = useState<TabOption[]>(config.tabs || []);

  // 정렬 상태
  const [sortBy, setSortBy] = useState<string | undefined>(undefined);
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');

  // 모달 상태 (detailMode === 'modal'일 때 사용)
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [selectedItem, setSelectedItem] = useState<T | null>(null);

  // 삭제 다이얼로그 상태
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [itemToDelete, setItemToDelete] = useState<T | null>(null);
  const [isBulkDelete, setIsBulkDelete] = useState(false);

  // 엑셀 다운로드 상태
  const [isExcelDownloading, setIsExcelDownloading] = useState(false);

  const itemsPerPage = config.itemsPerPage || 20;

  // 모바일 인피니티 스크롤 로딩 상태 (서버 사이드 페이지네이션 시)
  const [isMobileLoading, setIsMobileLoading] = useState(false);

  // 초기 데이터 로딩 완료 여부 (검색/필터 변경 시 전체 스켈레톤 방지)
  const isInitialFetchDone = useRef(false);

  // 서버 사이드 페이지네이션 상태 (API에서 반환하는 값)
  const [serverTotalCount, setServerTotalCount] = useState<number>(initialTotalCount || 0);
  const [serverTotalPages, setServerTotalPages] = useState<number>(1);

  // ===== 검색 Debounce (300ms) =====
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearchValue(searchValue);
      // 검색 변경 시 페이지를 1로 리셋 (서버 사이드 페이지네이션에서 올바른 결과 보장)
      setCurrentPage(1);
    }, 300);

    return () => clearTimeout(timer);
  }, [searchValue]);

  // ===== ID 추출 헬퍼 =====
  const getItemId = useCallback(
    (item: T): string => {
      if (typeof config.idField === 'function') {
        return config.idField(item);
      }
      return String(item[config.idField]);
    },
    [config.idField]
  );

  // ===== 데이터 필터링 =====
  // 서버 사이드 모드에서 searchFilter를 통한 클라이언트 사이드 검색 활성화 여부
  const isServerSearchFiltered = !config.clientSideFiltering && !!debouncedSearchValue && !!config.searchFilter;

  const filteredData = useMemo(() => {
    if (!config.clientSideFiltering) {
      // 서버 사이드 모드
      let serverData = rawData;

      // searchFilter가 정의되어 있으면 클라이언트 사이드 검색 적용 (백엔드 검색 미지원 대비)
      if (debouncedSearchValue && config.searchFilter) {
        serverData = rawData.filter((item) =>
          config.searchFilter!(item, debouncedSearchValue)
        );
      }

      // 서버 사이드에서도 컬럼 정렬 지원 (onSortChange 미정의 시 클라이언트 사이드 정렬)
      if (sortBy && !config.onSortChange) {
        serverData = [...serverData].sort((a, b) => {
          const aValue = (a as Record<string, unknown>)[sortBy];
          const bValue = (b as Record<string, unknown>)[sortBy];
          if (aValue == null && bValue == null) return 0;
          if (aValue == null) return sortOrder === 'asc' ? 1 : -1;
          if (bValue == null) return sortOrder === 'asc' ? -1 : 1;
          if (typeof aValue === 'number' && typeof bValue === 'number') {
            return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
          }
          const aStr = String(aValue);
          const bStr = String(bValue);
          const comparison = aStr.localeCompare(bStr, 'ko');
          return sortOrder === 'asc' ? comparison : -comparison;
        });
      }

      return serverData;
    }

    let filtered = [...rawData];

    // 커스텀 필터 함수 (filterConfig 기반 복잡한 필터링)
    if (config.customFilterFn) {
      filtered = config.customFilterFn(filtered, filters);
    }

    // 탭 필터
    if (activeTab !== 'all' && config.tabFilter) {
      filtered = filtered.filter((item) => config.tabFilter!(item, activeTab));
    }

    // 검색 필터 (debounced 값 사용)
    if (debouncedSearchValue && config.searchFilter) {
      filtered = filtered.filter((item) =>
        config.searchFilter!(item, debouncedSearchValue)
      );
    }

    // 날짜 범위 필터 (dateRangeSelector.dateField 설정 시 자동 적용)
    const { dateRangeSelector } = config;
    if (dateRangeSelector?.enabled && dateRangeSelector.dateField && dateRangeSelector.startDate && dateRangeSelector.endDate) {
      const dateField = dateRangeSelector.dateField;
      const start = new Date(dateRangeSelector.startDate);
      const end = new Date(dateRangeSelector.endDate);
      end.setHours(23, 59, 59, 999); // 종료일 끝까지 포함

      filtered = filtered.filter((item) => {
        const itemDate = (item as Record<string, unknown>)[dateField];
        if (!itemDate) return false;
        const date = new Date(String(itemDate));
        return date >= start && date <= end;
      });
    }

    // 커스텀 정렬 함수
    if (config.customSortFn) {
      filtered = config.customSortFn(filtered, filters);
    }

    // 컬럼 기반 정렬 (sortBy가 있을 때)
    if (sortBy) {
      filtered = [...filtered].sort((a, b) => {
        const aValue = (a as Record<string, unknown>)[sortBy];
        const bValue = (b as Record<string, unknown>)[sortBy];

        // null/undefined 처리
        if (aValue == null && bValue == null) return 0;
        if (aValue == null) return sortOrder === 'asc' ? 1 : -1;
        if (bValue == null) return sortOrder === 'asc' ? -1 : 1;

        // 숫자 비교
        if (typeof aValue === 'number' && typeof bValue === 'number') {
          return sortOrder === 'asc' ? aValue - bValue : bValue - aValue;
        }

        // 문자열 비교 (한글 지원)
        const aStr = String(aValue);
        const bStr = String(bValue);
        const comparison = aStr.localeCompare(bStr, 'ko');
        return sortOrder === 'asc' ? comparison : -comparison;
      });
    }

    return filtered;
  }, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.onSortChange, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]);

  // 페이지네이션 (클라이언트 사이드 + 서버 사이드 검색 시)
  const paginatedData = useMemo(() => {
    if (!config.clientSideFiltering) {
      // 서버 사이드 검색 시 클라이언트 사이드 페이지네이션 적용
      if (debouncedSearchValue && config.searchFilter) {
        const startIndex = (currentPage - 1) * itemsPerPage;
        return filteredData.slice(startIndex, startIndex + itemsPerPage);
      }
      return rawData;
    }
    const startIndex = (currentPage - 1) * itemsPerPage;
    return filteredData.slice(startIndex, startIndex + itemsPerPage);
  }, [config.clientSideFiltering, config.searchFilter, debouncedSearchValue, filteredData, currentPage, itemsPerPage, rawData]);

  // 총 개수 및 페이지 수
  // 서버 사이드 페이지네이션: API에서 반환한 값 사용
  // 클라이언트 사이드 페이지네이션: 로컬 데이터 길이 사용
  const totalCount = config.clientSideFiltering
    ? filteredData.length
    : (isServerSearchFiltered ? filteredData.length : serverTotalCount);
  const totalPages = config.clientSideFiltering
    ? Math.ceil(totalCount / itemsPerPage)
    : (isServerSearchFiltered ? (Math.ceil(filteredData.length / itemsPerPage) || 1) : serverTotalPages);

  // 삭제 등으로 현재 페이지가 빈 페이지가 되면 마지막 유효 페이지로 이동
  useEffect(() => {
    if (currentPage > 1 && (totalPages === 0 || currentPage > totalPages)) {
      setCurrentPage(Math.max(1, totalPages));
    }
  }, [totalPages, currentPage]);

  // 표시할 데이터
  // 서버 사이드 모드에서도 filteredData 사용 (클라이언트 사이드 정렬 반영)
  const displayData = (config.clientSideFiltering || isServerSearchFiltered) ? paginatedData : filteredData;

  // ===== 탭 카운트 계산 (클라이언트 사이드) =====
  const computedTabs = useMemo(() => {
    if (!config.clientSideFiltering || !config.tabs || !config.tabFilter) {
      return tabs;
    }

    return config.tabs.map((tab) => {
      if (tab.value === 'all') {
        return { ...tab, count: rawData.length };
      }
      const count = rawData.filter((item) => config.tabFilter!(item, tab.value)).length;
      return { ...tab, count };
    });
  }, [config.clientSideFiltering, config.tabs, config.tabFilter, rawData, tabs]);

  // ===== 데이터 로딩 =====
  // isMobileAppend: 모바일 인피니티 스크롤로 추가 로드 시 true
  const fetchData = useCallback(async (isMobileAppend = false) => {
    // 모바일 추가 로드면 isMobileLoading, 그 외에는 isLoading
    if (isMobileAppend) {
      setIsMobileLoading(true);
    } else if (!isInitialFetchDone.current) {
      // 초기 로딩 시에만 전체 스켈레톤 표시
      setIsLoading(true);
    }

    try {
      // 서버 사이드 + searchFilter 정의 + 검색 중: 전체 데이터를 받아서 클라이언트 사이드 필터링
      const useClientSearch = !config.clientSideFiltering && !!config.searchFilter && !!debouncedSearchValue;
      const result = await config.actions.getList(
        config.clientSideFiltering
          ? { pageSize: 9999 } // 클라이언트 사이드: 전체 데이터 로드
          : {
              page: useClientSearch ? 1 : currentPage,
              pageSize: useClientSearch ? 9999 : itemsPerPage,
              search: useClientSearch ? undefined : debouncedSearchValue,
              filters,
              tab: activeTab,
            }
      );

      if (result.success && result.data) {
        setRawData(result.data);
        // 서버 사이드 페이지네이션: API에서 반환한 totalCount, totalPages 저장
        if (!config.clientSideFiltering) {
          if (typeof result.totalCount === 'number') {
            setServerTotalCount(result.totalCount);
          }
          if (typeof result.totalPages === 'number') {
            setServerTotalPages(result.totalPages);
          }
        }
      } else {
        toast.error(result.error || '데이터를 불러오는데 실패했습니다.');
      }
    } catch (error) {
      console.error('[UniversalListPage] Fetch error:', error);
      toast.error('데이터를 불러오는데 실패했습니다.');
    } finally {
      setIsLoading(false);
      setIsMobileLoading(false);
      isInitialFetchDone.current = true;
    }
  }, [config.actions, config.clientSideFiltering, config.searchFilter, currentPage, itemsPerPage, debouncedSearchValue, filters, activeTab]);

  // 초기 로딩 (initialData가 없거나 빈 배열인 경우)
  useEffect(() => {
    if (!initialData || initialData.length === 0) {
      fetchData();
    }
  }, []);

  // initialData prop 변경 감지 (부모 컴포넌트에서 데이터 로드 후 전달하는 경우)
  // 삭제 후 빈 배열도 동기화해야 빈 페이지가 올바르게 표시됨
  useEffect(() => {
    if (initialData) {
      setRawData(initialData);
    }
  }, [initialData]);

  // config.tabs 변경 감지 (동적 탭 카운트 업데이트용)
  useEffect(() => {
    if (config.tabs) {
      setTabs(config.tabs);
    }
  }, [config.tabs]);

  // 선택 항목 변경 시 외부 콜백 호출
  useEffect(() => {
    if (config.onSelectionChange && !externalSelection) {
      config.onSelectionChange(selectedItems);
    }
  }, [selectedItems, config.onSelectionChange, externalSelection]);

  // 데이터 변경 콜백 (동적 컬럼 계산 등에 사용)
  // ⚠️ config.onDataChange를 deps에서 제외: 콜백 참조 변경으로 인한 무한 루프 방지
  useEffect(() => {
    config.onDataChange?.(rawData);
     
  }, [rawData]);

  // 서버 사이드 필터링: 의존성 변경 시 데이터 새로고침
  // 이전 페이지를 추적하여 모바일 인피니티 스크롤 감지
  const [prevPage, setPrevPage] = useState(1);

  // 날짜 범위 변경 감지용 (서버 사이드 필터링에서 날짜 변경 시 데이터 새로고침)
  const dateRangeKey = config.dateRangeSelector?.enabled
    ? `${config.dateRangeSelector.startDate || ''}-${config.dateRangeSelector.endDate || ''}`
    : '';

  useEffect(() => {
    if (!config.clientSideFiltering && !externalPagination && !isLoading && !isMobileLoading) {
      // 페이지가 증가하는 경우 = 모바일 인피니티 스크롤
      const isMobileAppend = currentPage > prevPage && currentPage > 1;
      fetchData(isMobileAppend);
      setPrevPage(currentPage);
    }
  }, [currentPage, debouncedSearchValue, filters, activeTab, dateRangeKey]);

  // 동적 탭 로딩
  useEffect(() => {
    if (config.fetchTabs) {
      config.fetchTabs().then((fetchedTabs) => {
        setTabs(fetchedTabs);
        if (!activeTab || activeTab === 'all') {
          setActiveTab(fetchedTabs[0]?.value || 'all');
        }
      });
    }
  }, [config.fetchTabs]);

  // ===== 선택 핸들러 =====
  // 외부 선택 상태 사용 시 외부 핸들러 사용
  const effectiveSelectedItems = externalSelection?.selectedItems ?? selectedItems;
  const effectiveGetItemId = externalSelection?.getItemId ?? getItemId;

  const toggleSelection = useCallback(
    (id: string) => {
      if (externalSelection) {
        externalSelection.onToggleSelection(id);
      } else {
        setSelectedItems((prev) => {
          const newSet = new Set(prev);
          if (newSet.has(id)) {
            newSet.delete(id);
          } else {
            newSet.add(id);
          }
          return newSet;
        });
      }
    },
    [externalSelection]
  );

  const toggleSelectAll = useCallback(() => {
    if (externalSelection) {
      externalSelection.onToggleSelectAll();
    } else {
      const currentData = displayData;
      if (selectedItems.size === currentData.length && currentData.length > 0) {
        setSelectedItems(new Set());
      } else {
        const allIds = new Set(currentData.map((item) => getItemId(item)));
        setSelectedItems(allIds);
      }
    }
  }, [externalSelection, displayData, selectedItems.size, getItemId]);

  // ===== 행 클릭 핸들러 =====
  const handleRowClick = useCallback(
    (item: T) => {
      const id = getItemId(item);
      const detailMode = config.detailMode || 'page';

      if (detailMode === 'modal') {
        setSelectedItem(item);
        setIsModalOpen(true);
      } else if (detailMode === 'page') {
        router.push(`/${locale}${config.basePath}/${id}?mode=view`);
      }
    },
    [config.basePath, config.detailMode, getItemId, locale, router]
  );

  const handleEdit = useCallback(
    (item: T) => {
      const id = getItemId(item);
      router.push(`/${locale}${config.basePath}/${id}?mode=edit`);
    },
    [config.basePath, getItemId, locale, router]
  );

  const handleCreate = useCallback(() => {
    router.push(`/${locale}${config.basePath}?mode=new`);
  }, [config.basePath, locale, router]);

  // ===== 삭제 핸들러 =====
  const handleDeleteClick = useCallback((item: T) => {
    setItemToDelete(item);
    setIsBulkDelete(false);
    setDeleteDialogOpen(true);
  }, []);

  const handleBulkDeleteClick = useCallback(() => {
    if (effectiveSelectedItems.size === 0) {
      toast.warning('삭제할 항목을 선택해주세요.');
      return;
    }
    setIsBulkDelete(true);
    setDeleteDialogOpen(true);
  }, [effectiveSelectedItems.size]);

  const handleDeleteConfirm = useCallback(async () => {
    try {
      if (isBulkDelete) {
        if (config.actions.deleteBulk) {
          const result = await config.actions.deleteBulk(Array.from(effectiveSelectedItems));
          if (result.success) {
            toast.success(`${effectiveSelectedItems.size}건이 삭제되었습니다.`);
            // 클라이언트 사이드: 로컬 데이터에서 제거
            if (config.clientSideFiltering) {
              setRawData((prev) =>
                prev.filter((item) => !effectiveSelectedItems.has(getItemId(item)))
              );
            } else {
              fetchData();
            }
            if (!externalSelection) {
              setSelectedItems(new Set());
            }
          } else {
            toast.error(result.error || '삭제에 실패했습니다.');
          }
        } else if (config.actions?.deleteItem) {
          const ids = Array.from(effectiveSelectedItems);
          let successCount = 0;
          for (const id of ids) {
            const result = await config.actions.deleteItem(id);
            if (result.success) successCount++;
          }
          toast.success(`${successCount}건이 삭제되었습니다.`);
          if (config.clientSideFiltering) {
            setRawData((prev) =>
              prev.filter((item) => !effectiveSelectedItems.has(getItemId(item)))
            );
          } else {
            fetchData();
          }
          if (!externalSelection) {
            setSelectedItems(new Set());
          }
        }
      } else if (itemToDelete) {
        if (config.actions?.deleteItem) {
          const id = getItemId(itemToDelete);
          const result = await config.actions.deleteItem(id);
          if (result.success) {
            toast.success('삭제되었습니다.');
            if (config.clientSideFiltering) {
              setRawData((prev) => prev.filter((item) => getItemId(item) !== id));
            } else {
              fetchData();
            }
          } else {
            toast.error(result.error || '삭제에 실패했습니다.');
          }
        }
      }
    } catch (error) {
      console.error('[UniversalListPage] Delete error:', error);
      toast.error('삭제 중 오류가 발생했습니다.');
    } finally {
      setDeleteDialogOpen(false);
      setItemToDelete(null);
    }
  }, [config.actions, config.clientSideFiltering, externalSelection, fetchData, getItemId, isBulkDelete, itemToDelete, effectiveSelectedItems]);

  // ===== 검색 핸들러 =====
  const handleSearchChange = useCallback((value: string) => {
    setSearchValue(value); // UI 즉시 반영
    setSearchStateValue('search', value); // sessionStorage + URL 동기화
  }, []);

  // 외부 콜백에 debounced 값 전달 (자체 loadData 관리하는 컴포넌트용)
  // config.onSearchChange: config 내부에서 설정한 검색 콜백도 호출
  // ⚠️ config.onSearchChange는 deps에서 제외 (config 재생성 → 무한 루프 방지, config.onDataChange 패턴 참고)
  useEffect(() => {
    onSearchChange?.(debouncedSearchValue);
    config.onSearchChange?.(debouncedSearchValue);
     
  }, [debouncedSearchValue, onSearchChange]);

  // ===== 필터 핸들러 =====
  const handleFilterChange = useCallback((key: string, value: string | string[]) => {
    const newFilters = { ...filters, [key]: value };
    setFilters(newFilters);
    setCurrentPage(1);
    setSelectedItems(new Set());
    // 외부 콜백 호출 (서버 사이드 필터링용)
    onFilterChangeCallback?.(newFilters);
  }, [filters, onFilterChangeCallback]);

  const handleFilterReset = useCallback(() => {
    setFilters(config.initialFilters || {});
    setCurrentPage(1);
    setSelectedItems(new Set());
  }, [config.initialFilters]);

  // ===== 엑셀 다운로드 핸들러 =====
  const handleExcelDownload = useCallback(async () => {
    if (!config.excelDownload) return;

    const { columns, filename = 'export', sheetName = 'Sheet1', fetchAllUrl, fetchAllParams, mapResponse } = config.excelDownload;

    setIsExcelDownloading(true);
    try {
      let dataToDownload: T[];

      // 디버깅: 데이터 개수 확인

      // fetchAllUrl이 있으면 서버에서 전체 데이터 페이지별 순차 조회 (clientSideFiltering 여부 무관)
      if (fetchAllUrl) {
        const PAGE_SIZE = 1000;

        // 동적 파라미터 구성
        const baseParams = new URLSearchParams();
        if (fetchAllParams) {
          const additionalParams = fetchAllParams({
            activeTab,
            filters,
            searchValue: debouncedSearchValue,
          });
          Object.entries(additionalParams).forEach(([key, value]) => {
            if (value) baseParams.append(key, value);
          });
        }

        // 1) 첫 페이지 호출로 total 확인
        const firstParams = new URLSearchParams(baseParams);
        firstParams.set('size', String(PAGE_SIZE));
        firstParams.set('per_page', String(PAGE_SIZE));
        firstParams.set('page', '1');

        const firstResponse = await fetch(`${fetchAllUrl}?${firstParams.toString()}`);
        const firstResult = await firstResponse.json();

        if (!firstResult.success) {
          throw new Error(firstResult.message || '데이터 조회에 실패했습니다.');
        }

        const firstPageData = mapResponse
          ? mapResponse(firstResult)
          : (firstResult.data?.data ?? firstResult.data ?? []);

        const total = firstResult.data?.total ?? firstPageData.length;
        const totalPages = Math.ceil(total / PAGE_SIZE);


        // 2) 나머지 페이지 병렬 호출
        const allData: T[] = [...(firstPageData as T[])];

        if (totalPages > 1) {
          const remainingPages = Array.from(
            { length: totalPages - 1 },
            (_, i) => i + 2
          );

          const pageResults = await Promise.all(
            remainingPages.map(async (page) => {
              const pageParams = new URLSearchParams(baseParams);
              pageParams.set('size', String(PAGE_SIZE));
              pageParams.set('per_page', String(PAGE_SIZE));
              pageParams.set('page', String(page));

              const res = await fetch(`${fetchAllUrl}?${pageParams.toString()}`);
              const result = await res.json();

              if (!result.success) {
                console.warn(`[Excel] 페이지 ${page} 조회 실패:`, result.message);
                return [];
              }

              return mapResponse
                ? mapResponse(result)
                : (result.data?.data ?? result.data ?? []);
            })
          );

          for (const pageData of pageResults) {
            allData.push(...(pageData as T[]));
          }
        }

        dataToDownload = allData;
      }
      // fetchAllUrl 없으면 현재 로드된 데이터 사용
      else {
        dataToDownload = rawData;
      }

      if (dataToDownload.length === 0) {
        toast.warning('다운로드할 데이터가 없습니다.');
        return;
      }

      await downloadExcel({
        data: dataToDownload as Record<string, unknown>[],
        columns: columns as ExcelColumn<Record<string, unknown>>[],
        filename,
        sheetName,
      });

      toast.success(`${dataToDownload.length}건 다운로드 완료`);
    } catch (error) {
      console.error('[Excel] 다운로드 실패:', error);
      toast.error(error instanceof Error ? error.message : '엑셀 다운로드에 실패했습니다.');
    } finally {
      setIsExcelDownloading(false);
    }
  }, [config.excelDownload, config.clientSideFiltering, filteredData, rawData, activeTab, filters, debouncedSearchValue]);

  // 선택 항목 엑셀 다운로드
  const handleSelectedExcelDownload = useCallback(async () => {
    if (!config.excelDownload) return;

    const { columns, filename = 'export', sheetName = 'Sheet1' } = config.excelDownload;
    const selectedIds = Array.from(effectiveSelectedItems);

    if (selectedIds.length === 0) {
      toast.warning('선택된 항목이 없습니다.');
      return;
    }

    // 현재 데이터에서 선택된 항목 필터링
    const selectedData = rawData.filter((item) => selectedIds.includes(getItemId(item)));

    await downloadSelectedExcel({
      data: selectedData as Record<string, unknown>[],
      columns: columns as ExcelColumn<Record<string, unknown>>[],
      selectedIds,
      idField: 'id',
      filename: `${filename}_선택`,
      sheetName,
    });

    toast.success(`${selectedData.length}건 다운로드 완료`);
  }, [config.excelDownload, effectiveSelectedItems, rawData, getItemId]);

  // 엑셀 전체 다운로드 버튼 (헤더 영역)
  const renderExcelDownloadButton = useMemo(() => {
    if (!config.excelDownload || config.excelDownload.enabled === false || !canExport) {
      return null;
    }

    return (
      <Button
        variant="outline"
        size="sm"
        onClick={handleExcelDownload}
        disabled={isExcelDownloading}
        className="gap-2"
      >
        {isExcelDownloading ? (
          <Loader2 className="h-4 w-4 animate-spin" />
        ) : (
          <Download className="h-4 w-4" />
        )}
        {isExcelDownloading ? '다운로드 중...' : '엑셀 다운로드'}
      </Button>
    );
  }, [config.excelDownload, canExport, isExcelDownloading, handleExcelDownload]);

  // 엑셀 선택 다운로드 버튼 (selectionActions 영역 - "전체 N건 / N개 항목 선택됨" 뒤)
  const renderExcelSelectedDownloadButton = useMemo(() => {
    if (!config.excelDownload || config.excelDownload.enabled === false || !canExport) {
      return null;
    }
    const { enableSelectedDownload = true } = config.excelDownload;
    if (!enableSelectedDownload || effectiveSelectedItems.size === 0) {
      return null;
    }

    return (
      <Button
        variant="outline"
        size="sm"
        onClick={handleSelectedExcelDownload}
        className="gap-2"
      >
        <Download className="h-4 w-4" />
        선택 다운로드 ({effectiveSelectedItems.size})
      </Button>
    );
  }, [config.excelDownload, canExport, effectiveSelectedItems.size, handleSelectedExcelDownload]);

  // ===== 정렬 핸들러 =====
  const handleSort = useCallback((key: string) => {
    let newSortBy: string | undefined;
    let newSortOrder: 'asc' | 'desc' = 'asc';

    if (sortBy === key) {
      // 같은 컬럼 클릭: asc → desc → 정렬 해제
      if (sortOrder === 'asc') {
        newSortBy = key;
        newSortOrder = 'desc';
      } else {
        newSortBy = undefined;
        newSortOrder = 'asc';
      }
    } else {
      // 다른 컬럼 클릭: 해당 컬럼으로 asc 정렬
      newSortBy = key;
      newSortOrder = 'asc';
    }

    setSortBy(newSortBy);
    setSortOrder(newSortOrder);
    setCurrentPage(1);

    // 서버 사이드 정렬: 부모 컴포넌트에 콜백 전달
    if (!config.clientSideFiltering && config.onSortChange) {
      config.onSortChange(newSortBy, newSortOrder);
    }
  }, [sortBy, sortOrder, config.clientSideFiltering, config.onSortChange]);

  // ===== 탭 핸들러 =====
  const handleTabChange = useCallback((value: string) => {
    setActiveTab(value);
    setCurrentPage(1);
    setSelectedItems(new Set());
    // 외부 콜백 호출 (서버 사이드 필터링용)
    onTabChange?.(value);
  }, [onTabChange]);

  // ===== 페이지네이션 핸들러 =====
  const handlePageChange = useCallback((page: number) => {
    setCurrentPage(page);
    setSelectedItems(new Set());
  }, []);

  // ===== 통계 카드 계산 =====
  const computedStats = useMemo(() => {
    if (config.computeStats) {
      return config.computeStats(
        config.clientSideFiltering ? rawData : displayData,
        config.clientSideFiltering ? rawData.length : totalCount
      );
    }
    return config.stats;
  }, [config.computeStats, config.stats, config.clientSideFiltering, rawData, displayData, totalCount]);

  // ===== 필터 값 변환 =====
  const filterValuesObj: FilterValues = useMemo(() => {
    return filters as FilterValues;
  }, [filters]);

  // ===== 탭별 컬럼 선택 + sortable 기본값 적용 =====
  const effectiveColumns = useMemo(() => {
    const baseColumns = config.columnsPerTab && activeTab && config.columnsPerTab[activeTab]
      ? config.columnsPerTab[activeTab]
      : config.columns;

    // sortable 기본값 적용:
    // - NON_SORTABLE_KEYS에 해당하는 키: 기본 false
    // - 그 외 모든 데이터 컬럼: 기본 true
    // - 명시적으로 지정된 값이 있으면 그 값 사용
    return baseColumns.map(col => ({
      ...col,
      sortable: col.sortable ?? !NON_SORTABLE_KEYS.includes(col.key),
    }));
  }, [config.columns, config.columnsPerTab, activeTab]);

  // ===== 컬럼 리사이즈 & 가시성 설정 (자동 활성화) =====
  const enableColumnSettings = !config.disableColumnSettings;
  const alwaysVisibleKeys = useMemo(
    () => effectiveColumns.filter(col => NON_SORTABLE_KEYS.includes(col.key)).map(col => col.key),
    [effectiveColumns]
  );
  const {
    visibleColumns: colSettingsVisible,
    allColumnsWithVisibility,
    columnWidths,
    setColumnWidth,
    toggleColumnVisibility,
    resetSettings,
    hasHiddenColumns,
  } = useColumnSettings({
    pageId: config.basePath,
    columns: effectiveColumns,
    alwaysVisibleKeys,
  });
  const hiddenColumnKeys = useMemo(
    () => allColumnsWithVisibility.filter(c => !c.visible).map(c => c.key),
    [allColumnsWithVisibility]
  );
  const templateColumns = enableColumnSettings ? colSettingsVisible : effectiveColumns;
  const templateColumnSettings = useMemo(() => {
    if (!enableColumnSettings) return undefined;
    return {
      columnWidths,
      onColumnResize: setColumnWidth,
      settingsPopover: (
        <ColumnSettingsPopover
          columns={allColumnsWithVisibility}
          onToggle={toggleColumnVisibility}
          onReset={resetSettings}
          hasHiddenColumns={hasHiddenColumns}
        />
      ),
    };
  }, [enableColumnSettings, columnWidths, setColumnWidth, allColumnsWithVisibility, toggleColumnVisibility, resetSettings, hasHiddenColumns]);

  // ===== ID로 아이템 찾기 헬퍼 =====
  const getItemById = useCallback(
    (id: string): T | undefined => {
      return rawData.find((item) => getItemId(item) === id);
    },
    [rawData, getItemId]
  );

  // ===== 페이지네이션 config =====
  // 외부 페이지네이션 사용 시 외부 설정 사용
  // 단, 서버 사이드 검색 모드(searchFilter)에서는 필터링된 데이터 기준으로 재계산
  const paginationConfig: PaginationConfig = useMemo(
    () => {
      if (isServerSearchFiltered && externalPagination) {
        return {
          ...externalPagination,
          currentPage,
          totalPages: Math.ceil(filteredData.length / itemsPerPage) || 1,
          totalItems: filteredData.length,
          onPageChange: handlePageChange,
        };
      }
      return externalPagination ?? {
        currentPage,
        totalPages,
        totalItems: totalCount,
        itemsPerPage,
        onPageChange: handlePageChange,
      };
    },
    [externalPagination, isServerSearchFiltered, filteredData.length, currentPage, totalPages, totalCount, itemsPerPage, handlePageChange]
  );

  // ===== 렌더링 함수 래퍼 =====
  const showCheckbox = config.showCheckbox !== false;
  const renderTableRow = useCallback(
    (item: T, index: number, globalIndex: number) => {
      const id = effectiveGetItemId(item);
      const isSelected = effectiveSelectedItems.has(id);
      const row = config.renderTableRow(item, index, globalIndex, {
        isSelected,
        onToggle: () => toggleSelection(id),
        onRowClick: () => handleRowClick(item),
        onEdit: () => handleEdit(item),
        onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
      });

      // 컬럼 설정 활성화 시 숨긴 컬럼의 셀을 React.Children으로 제거
      if (!enableColumnSettings || hiddenColumnKeys.length === 0) return row;
      if (!React.isValidElement(row)) return row;

      const children = React.Children.toArray((row as React.ReactElement<{ children?: React.ReactNode }>).props.children);
      const offset = showCheckbox ? 1 : 0;
      const filtered = children.filter((_, cellIndex) => {
        if (showCheckbox && cellIndex === 0) return true; // 체크박스 유지
        const col = effectiveColumns[cellIndex - offset];
        return !col || !hiddenColumnKeys.includes(col.key);
      });
      return React.cloneElement(row as React.ReactElement, {}, ...filtered);
    },
    [config, effectiveGetItemId, handleDeleteClick, handleEdit, handleRowClick, effectiveSelectedItems, toggleSelection, enableColumnSettings, hiddenColumnKeys, showCheckbox, effectiveColumns]
  );

  const renderMobileCard = useCallback(
    (item: T, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => {
      return config.renderMobileCard(item, index, globalIndex, {
        isSelected,
        onToggle,
        onRowClick: () => handleRowClick(item),
        onEdit: () => handleEdit(item),
        onDelete: permCanDelete ? () => handleDeleteClick(item) : undefined,
      });
    },
    [config, handleDeleteClick, handleEdit, handleRowClick]
  );

  // ===== 삭제 확인 메시지 =====
  const deleteConfirmTitle = config.deleteConfirmMessage?.title || '삭제 확인';
  const deleteConfirmDescription =
    config.deleteConfirmMessage?.description ||
    (isBulkDelete
      ? `선택한 ${effectiveSelectedItems.size}건을 삭제하시겠습니까?`
      : '이 항목을 삭제하시겠습니까?');

  return (
    <>
      <IntegratedListTemplateV2
        // 페이지 헤더
        title={config.title}
        description={config.description}
        icon={config.icon}
        headerActions={
          <>
            {/* 엑셀 다운로드 버튼 (config.excelDownload 설정 시 자동 추가) */}
            {renderExcelDownloadButton}
            {/* 커스텀 헤더 액션 */}
            {config.headerActions?.({
              onCreate: handleCreate,
              selectedItems: effectiveSelectedItems,
              onClearSelection: () => setSelectedItems(new Set()),
              onRefresh: fetchData,
            })}
          </>
        }
        // 공통 헤더 옵션 (달력/등록버튼)
        dateRangeSelector={config.dateRangeSelector}
        createButton={permCanCreate ? config.createButton : undefined}
        // 탭 콘텐츠
        tabsContent={config.tabsContent}
        // 통계 카드
        stats={computedStats}
        // 경고 배너
        alertBanner={config.alertBanner}
        // 검색 및 필터
        // hideSearch: true이면서 config에 onSearchChange/searchFilter가 없으면 검색 완전 비활성화
        // hideSearch: true이면서 onSearchChange/searchFilter가 있으면 헤더 검색창만 표시 (Card SearchFilter 숨김)
        searchValue={(config.hideSearch && !config.onSearchChange && !config.searchFilter) ? undefined : searchValue}
        onSearchChange={(config.hideSearch && !config.onSearchChange && !config.searchFilter) ? undefined : handleSearchChange}
        searchPlaceholder={config.searchPlaceholder}
        extraFilters={config.extraFilters}
        hideSearch={config.hideSearch}
        // 탭 (빈 배열일 때는 undefined로 전달해서 IntegratedListTemplateV2의 기본 탭 사용)
        tabs={computedTabs.length > 0 ? computedTabs : undefined}
        activeTab={activeTab}
        onTabChange={handleTabChange}
        tabsPosition={config.tabsPosition}
        // 필터 시스템
        filterConfig={config.filterConfig}
        filterValues={filterValuesObj}
        onFilterChange={handleFilterChange}
        onFilterReset={handleFilterReset}
        filterTitle={config.filterTitle}
        // 테이블 앞 콘텐츠 (함수일 경우 params 전달)
        beforeTableContent={
          typeof config.beforeTableContent === 'function'
            ? config.beforeTableContent({
                selectedItems: effectiveSelectedItems,
                onClearSelection: () => externalSelection ? undefined : setSelectedItems(new Set()),
              })
            : config.beforeTableContent
        }
        // 테이블 헤더 액션 (함수일 경우 params 전달)
        tableHeaderActions={
          typeof config.tableHeaderActions === 'function'
            ? config.tableHeaderActions({
                totalCount: isServerSearchFiltered ? filteredData.length : (externalPagination?.totalItems ?? totalCount),
                selectedItems: effectiveSelectedItems,
                onClearSelection: () => externalSelection ? undefined : setSelectedItems(new Set()),
              })
            : config.tableHeaderActions
        }
        // 테이블 컬럼 (가시성 필터링 적용)
        tableColumns={templateColumns}
        // 컬럼 리사이즈 & 가시성 설정
        columnSettings={templateColumnSettings}
        // 정렬 설정 (모든 페이지에서 활성화)
        sortBy={sortBy}
        sortOrder={sortOrder}
        onSort={handleSort}
        // 커스텀 테이블 헤더 (동적 컬럼용)
        renderCustomTableHeader={
          config.renderCustomTableHeader
            ? () =>
                config.renderCustomTableHeader!({
                  displayData,
                  selectedItems: effectiveSelectedItems,
                  onToggleSelectAll: toggleSelectAll,
                })
            : undefined
        }
        // 테이블 푸터
        tableFooter={config.tableFooter}
        // 테이블 뒤 컨텐츠 (캘린더 등)
        afterTableContent={
          typeof config.afterTableContent === 'function'
            ? config.afterTableContent({ data: displayData, selectedItems: effectiveSelectedItems })
            : config.afterTableContent
        }
        // 데이터
        data={displayData}
        totalCount={totalCount}
        allData={config.clientSideFiltering ? filteredData : undefined}
        // 모바일 인피니티 스크롤 로딩 상태
        isMobileLoading={isMobileLoading}
        // 체크박스 선택
        selectedItems={effectiveSelectedItems}
        onToggleSelection={toggleSelection}
        onToggleSelectAll={toggleSelectAll}
        getItemId={effectiveGetItemId}
        onBulkDelete={config.actions?.deleteItem ? handleBulkDeleteClick : undefined}
        selectionActions={
          <>
            {renderExcelSelectedDownloadButton}
            {config.selectionActions?.({
              selectedItems: effectiveSelectedItems,
              onClearSelection: () => externalSelection ? undefined : setSelectedItems(new Set()),
              onRefresh: fetchData,
            })}
          </>
        }
        // 표시 옵션
        showCheckbox={config.showCheckbox}
        showRowNumber={config.showRowNumber}
        // 렌더링 함수
        renderTableRow={renderTableRow}
        renderMobileCard={renderMobileCard}
        // 페이지네이션
        pagination={paginationConfig}
        // 로딩 상태 (외부 로딩 상태 우선 사용)
        isLoading={externalIsLoading ?? isLoading}
      />

      {/* 삭제 확인 다이얼로그 */}
      <DeleteConfirmDialog
        open={deleteDialogOpen}
        onOpenChange={setDeleteDialogOpen}
        onConfirm={handleDeleteConfirm}
        title={deleteConfirmTitle}
        description={deleteConfirmDescription}
      />

      {/* 상세 모달 (detailMode === 'modal'일 때) */}
      {config.detailMode === 'modal' && config.DetailModalComponent && (
        <config.DetailModalComponent
          isOpen={isModalOpen}
          onClose={() => {
            setIsModalOpen(false);
            setSelectedItem(null);
          }}
          item={selectedItem}
          onRefresh={fetchData}
        />
      )}

      {/* 커스텀 다이얼로그 슬롯 */}
      {config.renderDialogs?.({
        data: displayData,
        selectedItems: effectiveSelectedItems,
        activeTab,
        onRefresh: fetchData,
        getItemById,
      })}
    </>
  );
}

// 타입 re-export
export * from './types';