import { useState, useEffect, useMemo, useRef } from 'react';
import type { DateRange } from 'react-day-picker';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { PageHeader } from '@/components/shared/PageHeader';
import { EmptyState } from '@/components/shared/EmptyState';
import { Skeleton } from '@/components/ui/skeleton';
import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
  ChevronRight, Download, Upload, Trash2, Pencil,
  ArrowUpDown, ArrowUp, ArrowDown, ChevronLeft, SlidersHorizontal,
  LoaderCircle, Sparkles,
} from 'lucide-react';
import { Checkbox } from '@/components/ui/checkbox';
import { ProcurementMaterial } from '@/types';
import { ColumnConfigDialog } from '@/components/shared/ColumnConfigDialog';
import { ExportDialog } from '@/components/shared/ExportDialog';
import { BatchIdCombobox } from '@/components/shared/BatchIdCombobox';
import { useProcurementBatchOptions } from '@/hooks/useProcurementBatchOptions';
import { saveStoredColumnConfig } from '@/lib/columnConfigStorage';
import { computeTableMinWidth } from '@/lib/tableLayout';
import { useAuth } from '@/hooks/useAuth';
import { useStandardizationQueue } from '@/contexts/StandardizationQueueContext';
import { inferStandardizationExecutionMode } from '@/lib/standardizationExecution';
import { formatDate } from '@/lib/dateUtils';
import { isSupabaseConfigured } from '@/lib/supabase';
import {
  ACCEPTED_EXTENSIONS,
  ACCEPTED_TYPES,
  MATERIAL_COLUMN_STORAGE_KEY,
  MAX_FILE_SIZE_BYTES,
  MaterialDetailPanel,
  MaterialEditDialog,
  MaterialFilterBar,
  PAGE_SIZE_OPTIONS,
  SourceFileViewDialog,
  StandardizeConfirmDialog,
  formatCreatedAtRangeValue,
  formatDateForFilter,
  getInitialMaterialColumnConfig,
  parseCreatedAtRange,
  parseDateValue,
  useFilteredMaterials,
  useMaterialMutations,
  useMaterialsList,
} from '@/features/materials';

type SortDir = 'asc' | 'desc';

export default function Materials() {
  const { user } = useAuth();
  const [loading] = useState(false);
  const [searchBatchId, setSearchBatchId] = useState('');
  const [searchSourceFile, setSearchSourceFile] = useState('');
  const [searchName, setSearchName] = useState('');
  const [searchCreatedBy, setSearchCreatedBy] = useState('');
  const [searchCreatedAtRange, setSearchCreatedAtRange] = useState('');
  const [searchCreatedAtFrom, setSearchCreatedAtFrom] = useState('');
  const [searchCreatedAtTo, setSearchCreatedAtTo] = useState('');
  const [createdAtPickerOpen, setCreatedAtPickerOpen] = useState(false);
  const [searchExpanded, setSearchExpanded] = useState(false);
  const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
  const [sortKey, setSortKey] = useState<string | null>(null);
  const [sortDir, setSortDir] = useState<SortDir>('asc');
  const [page, setPage] = useState(1);
  const [pageSize, setPageSize] = useState(20);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
  const fileInputRef = useRef<HTMLInputElement>(null);
  const [fileDialogBatchId, setFileDialogBatchId] = useState<string | null>(null);
  const [uploadBatchId, setUploadBatchId] = useState<string>('');
  const [hoveredCol, setHoveredCol] = useState<string | null>(null);
  const [resizingCol, setResizingCol] = useState<string | null>(null);
  const [configOpen, setConfigOpen] = useState(false);
  const [exportOpen, setExportOpen] = useState(false);
  const { materials, supabaseMaterials: _supabaseMaterials, uploadedMaterials, setUploadedMaterials } = useMaterialsList();
  const [columnConfig, setColumnConfig] = useState(() => getInitialMaterialColumnConfig());
  const { enqueueOnlineTask } = useStandardizationQueue();
  const [standardizeConfirm, setStandardizeConfirm] = useState<{
    open: boolean;
    totalRows: number;
    executionMode: 'online' | 'offline';
    batchId: string;
    materialIds?: string[];
    standardizeMaterials?: Array<{
      id: string; material_name: string; specification: string;
      quantity: number; unit: string; invoice_type: string; brand: string;
      remarks: string; delivery_address: string;
    }>;
  }>({ open: false, totalRows: 0, executionMode: 'online', batchId: '' });
  const { visibleCols, orderedKeys: orderedColKeys } = columnConfig;
  const [colWidths, setColWidths] = useState<Record<string, number>>({
    batch_id: 176, material_name: 150, specification: 160,
    quantity: 80, unit: 64, brand: 64, source_file: 160,
    created_by: 80, created_at: 140, _actions: 140,
  });
  const resizingRef = useRef<{ key: string; startX: number; startWidth: number } | null>(null);
  const justResizedRef = useRef(false);

  function handleStartStandardize() {
    // Derive batch_id: prefer search filter, then upload batch, then from selected rows
    const selectedMaterials = materials.filter(r => selectedKeys.has(r.id));
    const selectedBatchIds = [...new Set(selectedMaterials.map(item => item.batch_id).filter(Boolean))];
    if (!searchBatchId && !uploadBatchId && selectedBatchIds.length > 1) {
      toast.error('暂不支持跨批次同时执行 AI 标准化，请只选择同一批次的数据');
      return;
    }

    const batchId = searchBatchId || uploadBatchId || (selectedBatchIds[0] ?? '');
    if (!batchId) {
      toast.error('请先选择一个批次，或先勾选数据行');
      return;
    }

    const batchMaterials = materials.filter(item => item.batch_id === batchId);
    const selectedBatchMaterials = selectedMaterials.length > 0
      ? selectedMaterials.filter(item => item.batch_id === batchId)
      : [];
    const totalRows = selectedBatchMaterials.length > 0 ? selectedBatchMaterials.length : batchMaterials.length;
    const materialIds = selectedBatchMaterials.length > 0 ? selectedBatchMaterials.map(r => r.id) : undefined;
    const standardizeMaterials = selectedBatchMaterials.length > 0
      ? selectedBatchMaterials.map((item) => ({
        id: item.id,
        material_name: item.material_name,
        specification: item.specification,
        quantity: item.quantity,
        unit: item.unit,
        invoice_type: item.invoice_type,
        brand: item.brand,
        remarks: item.remarks,
        delivery_address: item.delivery_address,
      }))
      : undefined;

    if (selectedMaterials.length > 0 && selectedBatchMaterials.length !== selectedMaterials.length) {
      toast.error('选中的数据包含其他批次，请只保留同一批次后再试');
      return;
    }

    const executionMode = inferStandardizationExecutionMode(totalRows);

    // Show confirmation dialog instead of executing immediately
    setStandardizeConfirm({
      open: true,
      totalRows,
      executionMode,
      batchId,
      materialIds,
      standardizeMaterials,
    });
  }

  async function executeStandardize() {
    const { totalRows, batchId, materialIds, standardizeMaterials } = standardizeConfirm;
    setStandardizeConfirm(prev => ({ ...prev, open: false }));

    try {
      const result = enqueueOnlineTask({
        batchId,
        materialIds,
        materials: standardizeMaterials,
        totalRows,
      });

      if (!result.accepted && result.duplicate) {
        toast.info('相同的在线标准化任务已在队列中，请勿重复提交。', {
          duration: 3500,
          position: 'bottom-right',
        });
        return;
      }

      toast.success(
        result.aheadCount > 0
          ? `在线标准化已加入队列，前方还有 ${result.aheadCount} 个任务。`
          : '在线标准化已加入队列，正在准备执行。',
        {
          duration: 5000,
          position: 'bottom-right',
        },
      );
    } catch (err) {
      toast.error(err instanceof Error ? err.message : '启动标准化失败');
    }
  }

  const { batches: procurementBatches, batchOptions: procurementBatchOptions } = useProcurementBatchOptions();

  // ── 编辑材料状态（mutation hook 需要 setEditingMaterial 在编辑保存成功时关闭弹窗） ──
  const [editingMaterial, setEditingMaterial] = useState<ProcurementMaterial | null>(null);
  const [editForm, setEditForm] = useState<Partial<ProcurementMaterial>>({});

  function openEditDialog(row: ProcurementMaterial) {
    setEditingMaterial(row);
    setEditForm({ ...row });
  }

  const { uploadMutation, deleteMutation, batchDeleteMutation, updateMutation } = useMaterialMutations({
    getUploadBatchId: () => uploadBatchId,
    getUser: () => user,
    uploadedMaterials,
    setUploadedMaterials,
    getMaterials: () => materials,
    setExpandedIds,
    setSelectedKeys,
    setEditingMaterial,
    onUploadSuccess: batchId => {
      setSearchBatchId(batchId);
      setSearchSourceFile('');
      setSearchName('');
      setSearchCreatedBy('');
      setSearchCreatedAtRange('');
      setSearchCreatedAtFrom('');
      setSearchCreatedAtTo('');
      setPage(1);
    },
  });

  function handleSaveEdit() {
    if (!editingMaterial) return;
    const { id: _id, created_at: _createdAt, ...rest } = editForm as ProcurementMaterial;
    updateMutation.mutate({ id: editingMaterial.id, updates: rest });
  }

  useEffect(() => {
    saveStoredColumnConfig(MATERIAL_COLUMN_STORAGE_KEY, {
      visibleKeys: orderedColKeys.filter(key => visibleCols.has(key)),
      orderedKeys: orderedColKeys,
    });
  }, [orderedColKeys, visibleCols]);

  function beginResize(key: string, startX: number) {
    justResizedRef.current = false;
    setResizingCol(key);
    resizingRef.current = { key, startX, startWidth: colWidths[key] ?? 120 };
    const previousCursor = document.body.style.cursor;
    const previousUserSelect = document.body.style.userSelect;
    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';

    function onMouseMove(ev: MouseEvent) {
      if (!resizingRef.current) return;
      justResizedRef.current = true;
      const newWidth = Math.max(40, resizingRef.current.startWidth + ev.clientX - resizingRef.current.startX);
      setColWidths(prev => ({ ...prev, [resizingRef.current!.key]: newWidth }));
    }

    function onMouseUp() {
      resizingRef.current = null;
      setResizingCol(null);
      document.body.style.cursor = previousCursor;
      document.body.style.userSelect = previousUserSelect;
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
    }

    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  }

  function startResize(key: string, e: React.MouseEvent) {
    e.preventDefault();
    e.stopPropagation();
    beginResize(key, e.clientX);
  }

  function isResizeEdgeHit(element: HTMLElement, clientX: number) {
    const rect = element.getBoundingClientRect();
    return rect.right - clientX <= 12;
  }

  function toggleExpand(id: string, e: React.MouseEvent) {
    e.stopPropagation();
    setExpandedIds(prev => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }

  function handleSort(key: string) {
    if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortKey(key); setSortDir('asc'); }
  }

  function handleUploadClick() {
    if (!isSupabaseConfigured()) {
      toast.error('请先在 .env.local 中配置 Supabase 环境变量。');
      return;
    }

    if (!uploadBatchId) {
      toast.error('请先选择归属批次后再上传文件。');
      return;
    }

    fileInputRef.current?.click();
  }

  function handleUploadBatchChange(nextBatchId: string) {
    const previousUploadBatchId = uploadBatchId;

    setUploadBatchId(nextBatchId);
    setSearchBatchId(currentSearchBatchId => {
      if (nextBatchId) {
        return nextBatchId;
      }

      if (previousUploadBatchId && currentSearchBatchId === previousUploadBatchId) {
        return '';
      }

      return currentSearchBatchId;
    });
    setPage(1);
  }

  function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const files = Array.from(e.target.files ?? []);
    if (files.length === 0) return;
    const oversized = files.filter(file => file.size > MAX_FILE_SIZE_BYTES);
    if (oversized.length > 0) {
      toast.error(`以下文件超过 20MB 限制：${oversized.map(file => file.name).join('、')}`);
      e.target.value = '';
      return;
    }
    const invalid = files.filter(f => !ACCEPTED_TYPES.includes(f.type));
    if (invalid.length > 0) {
      toast.error(`不支持的文件类型：${invalid.map(f => f.name).join('、')}`);
      e.target.value = '';
      return;
    }

    uploadMutation.mutate(files);
    e.target.value = '';
  }

  function handleCreatedAtRangeChange(value: string) {
    setSearchCreatedAtRange(value);
    const parsed = parseCreatedAtRange(value);
    if (!parsed) return;
    setSearchCreatedAtFrom(parsed.from);
    setSearchCreatedAtTo(parsed.to);
    setPage(1);
  }

  function handleCreatedAtRangeSelect(range: DateRange | undefined) {
    const from = range?.from ? formatDateForFilter(range.from) : '';
    const to = range?.to ? formatDateForFilter(range.to) : '';
    setSearchCreatedAtFrom(from);
    setSearchCreatedAtTo(to);
    setSearchCreatedAtRange(formatCreatedAtRangeValue(from, to));
    setPage(1);
    if (range?.from && range?.to) {
      setCreatedAtPickerOpen(false);
    }
  }

  function handleResetFilters() {
    setSearchBatchId('');
    setSearchSourceFile('');
    setSearchName('');
    setSearchCreatedBy('');
    setSearchCreatedAtRange('');
    setSearchCreatedAtFrom('');
    setSearchCreatedAtTo('');
    setPage(1);
  }

  const uploadBatchOptions = procurementBatchOptions;
  const selectedCreatedAtRange = useMemo<DateRange | undefined>(() => {
    const from = parseDateValue(searchCreatedAtFrom);
    const to = parseDateValue(searchCreatedAtTo);
    if (!from && !to) return undefined;
    return { from, to };
  }, [searchCreatedAtFrom, searchCreatedAtTo]);

  const filtered = useFilteredMaterials(materials, {
    batchId: searchBatchId,
    sourceFile: searchSourceFile,
    name: searchName,
    createdBy: searchCreatedBy,
    createdAtFrom: searchCreatedAtFrom,
    createdAtTo: searchCreatedAtTo,
    sortKey,
    sortDir,
  });

  const exportData = selectedKeys.size > 0
    ? filtered.filter(r => selectedKeys.has(r.id))
    : filtered;

  const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
  const safePage = Math.min(page, totalPages);
  const paged = filtered.slice((safePage - 1) * pageSize, safePage * pageSize);

  function SortIcon({ col }: { col: string }) {
    if (sortKey !== col) return <ArrowUpDown className="h-3 w-3 opacity-30 ml-1" />;
    return sortDir === 'asc'
      ? <ArrowUp className="h-3 w-3 ml-1" />
      : <ArrowDown className="h-3 w-3 ml-1" />;
  }

  const COLS = [
    { key: '_index', label: '序号', required: true, className: 'w-12 px-2' },
    { key: 'batch_id', label: '批次ID', sortable: true },
    { key: 'material_name', label: '材料名称' },
    { key: 'specification', label: '规格型号' },
    { key: 'quantity', label: '数量' },
    { key: 'unit', label: '单位' },
    { key: 'brand', label: '品牌' },
    { key: 'source_file', label: '来源文件' },
    { key: 'created_by', label: '创建人', sortable: true },
    { key: 'created_at', label: '创建时间', sortable: true },
    { key: '_actions', label: '操作', required: true },
  ];
  const visibleCOLS = orderedColKeys
    .map(key => COLS.find(col => col.key === key))
    .filter((col): col is (typeof COLS)[number] => Boolean(col && visibleCols.has(col.key)));

  useEffect(() => {
    setColWidths(prev => {
      const next: Record<string, number> = {
        ...prev,
        ...Object.fromEntries(visibleCOLS.map(col => [col.key, prev[col.key] ?? (col.key === 'batch_id' ? 176 : 120)])),
        _actions: prev._actions ?? 100,
      };
      const prevKeys = Object.keys(prev);
      const nextKeys = Object.keys(next);

      if (prevKeys.length === nextKeys.length && nextKeys.every(key => prev[key] === next[key])) {
        return prev;
      }

      return next;
    });
  }, [visibleCOLS]);

  const tableMinWidth = useMemo(() => computeTableMinWidth({
    columnKeys: visibleCOLS.map(col => col.key),
    widths: colWidths,
    fallbackWidth: 120,
    fixedWidths: [40, 56, colWidths._actions ?? 100],
    ignoredKeys: ['_index', '_actions'],
  }), [colWidths, visibleCOLS]);

  function renderDataCell(row: ProcurementMaterial, key: string) {
    switch (key) {
      case '_index':
        return null;
      case 'batch_id':
        return (
          <TableCell key={key} className="overflow-hidden whitespace-nowrap">
            <span className="font-mono text-xs text-muted-foreground whitespace-nowrap" title={row.batch_id}>
              {row.batch_id}
            </span>
          </TableCell>
        );
      case 'material_name':
        return (
          <TableCell key={key} className="overflow-hidden whitespace-nowrap">
            <span className="block truncate font-medium" title={row.material_name}>{row.material_name}</span>
          </TableCell>
        );
      case 'specification':
        return (
          <TableCell key={key} className="overflow-hidden text-sm whitespace-nowrap" title={row.specification}>
            <span className="block truncate">{row.specification}</span>
          </TableCell>
        );
      case 'quantity':
        return (
          <TableCell key={key} className="overflow-hidden whitespace-nowrap">
            <span className="block truncate">{row.quantity}</span>
          </TableCell>
        );
      case 'unit':
        return (
          <TableCell key={key} className="overflow-hidden whitespace-nowrap">
            <span className="block truncate" title={row.unit}>{row.unit}</span>
          </TableCell>
        );
      case 'brand':
        return (
          <TableCell key={key} className="overflow-hidden whitespace-nowrap">
            <span className="block truncate" title={row.brand}>{row.brand}</span>
          </TableCell>
        );
      case 'source_file':
        return (
          <TableCell key={key} className="overflow-hidden text-sm whitespace-nowrap">
            <button
              type="button"
              className="block w-full truncate whitespace-nowrap text-left text-foreground transition-colors hover:text-blue-600"
              title={row.source_file}
              onClick={e => { e.stopPropagation(); setFileDialogBatchId(row.batch_id); }}
            >
              {row.source_file}
            </button>
          </TableCell>
        );
      case 'created_by':
        return (
          <TableCell key={key} className="overflow-hidden text-sm whitespace-nowrap">
            <span className="block truncate" title={row.created_by ?? '-'}>{row.created_by ?? '-'}</span>
          </TableCell>
        );
      case 'created_at':
        return (
          <TableCell key={key} className="overflow-hidden text-sm whitespace-nowrap">
            <span className="block truncate">{formatDate(row.created_at)}</span>
          </TableCell>
        );
      default:
        return null;
    }
  }

  return (
    <div className="h-full flex flex-col">
      <PageHeader title="采购清单" />

      {loading ? (
        <div className="space-y-3">
          {Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-10 w-full" />)}
        </div>
      ) : (
        <div className="flex-1 flex flex-col min-h-0">
          {/* 搜索栏 */}
          <MaterialFilterBar
            searchBatchId={searchBatchId}
            searchSourceFile={searchSourceFile}
            searchName={searchName}
            searchCreatedBy={searchCreatedBy}
            searchCreatedAtRange={searchCreatedAtRange}
            searchExpanded={searchExpanded}
            createdAtPickerOpen={createdAtPickerOpen}
            selectedCreatedAtRange={selectedCreatedAtRange}
            procurementBatchOptions={procurementBatchOptions}
            onSearchBatchIdChange={v => { setSearchBatchId(v); setPage(1); }}
            onSearchSourceFileChange={v => { setSearchSourceFile(v); setPage(1); }}
            onSearchNameChange={v => { setSearchName(v); setPage(1); }}
            onSearchCreatedByChange={v => { setSearchCreatedBy(v); setPage(1); }}
            onCreatedAtRangeChange={handleCreatedAtRangeChange}
            onCreatedAtRangeSelect={handleCreatedAtRangeSelect}
            onSearchExpandedChange={setSearchExpanded}
            onCreatedAtPickerOpenChange={setCreatedAtPickerOpen}
            onResetFilters={handleResetFilters}
          />

          {/* 分割线 */}
          <div className="border-t border-gray-300 mb-3 shrink-0" />

          {/* 操作栏 */}
          <div className="mb-3 flex items-center gap-2 shrink-0">
            <input ref={fileInputRef} type="file" multiple accept={ACCEPTED_EXTENSIONS} className="hidden" onChange={handleFileChange} />
            <Button
              size="sm"
              className="h-9 bg-gradient-to-r from-violet-500 to-indigo-500 hover:from-violet-600 hover:to-indigo-600 text-white"
              onClick={handleStartStandardize}
              disabled={!searchBatchId && !uploadBatchId && selectedKeys.size === 0}
            >
              <Sparkles className="h-4 w-4 mr-1" />AI 标准化
            </Button>
            <BatchIdCombobox
              value={uploadBatchId}
              onChange={handleUploadBatchChange}
              options={uploadBatchOptions}
              placeholder="输入批次ID或批次名称"
              closedPlaceholder="请选择归属批次"
              allLabel={null}
              emptyText={procurementBatches.length === 0 ? '暂无批次，请先在批次管理中创建' : '无匹配批次'}
              className="w-56"
            />
            <Button size="sm" className="h-9" onClick={handleUploadClick} disabled={uploadMutation.isPending || !uploadBatchId}>
              {uploadMutation.isPending ? <LoaderCircle className="h-4 w-4 mr-1 animate-spin" /> : <Upload className="h-4 w-4 mr-1" />}
              上传文件
            </Button>
            <Button variant="outline" size="sm" className="h-9" onClick={() => setExportOpen(true)}>
              <Download className="h-4 w-4 mr-1" />导出
            </Button>
            <div className="ml-auto flex items-center gap-1">
              <Button
                variant="destructive"
                size="sm"
                className="h-9"
                disabled={selectedKeys.size === 0 || batchDeleteMutation.isPending}
                onClick={() => {
                  const count = selectedKeys.size;
                  if (!window.confirm(`确认批量删除选中的 ${count} 条材料吗？`)) return;
                  batchDeleteMutation.mutate([...selectedKeys]);
                }}
              >
                {batchDeleteMutation.isPending
                  ? <LoaderCircle className="h-4 w-4 mr-1 animate-spin" />
                  : <Trash2 className="h-4 w-4 mr-1" />}
                批量删除
              </Button>
              <Button variant="outline" size="sm" className="h-9" onClick={() => setConfigOpen(true)}>
                <SlidersHorizontal className="h-3.5 w-3.5 mr-1" />配置
              </Button>
            </div>
          </div>

          {filtered.length === 0 ? (
            <div className="flex-1 rounded-lg border border-border">
              <EmptyState message="暂无采购清单，请先上传文件或调整筛选条件" />
            </div>
          ) : (
            <div className="flex-1 min-h-0 overflow-auto rounded-lg border border-border [&_th]:h-8 [&_th]:py-0 [&_td]:py-1.5 [&_td]:px-3 [&_th]:px-3">
              <Table className="w-max min-w-full" style={{ tableLayout: 'fixed', minWidth: tableMinWidth }}>
                <TableHeader className="sticky top-0 z-10">
                  <TableRow className="bg-gray-100 hover:bg-gray-100">
                    <TableHead style={{ width: 40, left: 0 }} className="sticky z-20 bg-gray-100">
                      <Checkbox
                        checked={paged.length > 0 && paged.every(row => selectedKeys.has(row.id))}
                        onCheckedChange={checked => {
                          const next = new Set(selectedKeys);
                          paged.forEach(row => {
                            if (checked) {
                              next.add(row.id);
                            } else {
                              next.delete(row.id);
                            }
                          });
                          setSelectedKeys(next);
                        }}
                        aria-label="全选当前页"
                      />
                    </TableHead>
                    {visibleCOLS.filter(col => col.key !== '_actions').map(col => (
                      <TableHead
                        key={col.key}
                        style={col.key === '_index' ? { width: 56, left: 40 } : { width: colWidths[col.key] }}
                        className={`text-xs font-semibold relative ${col.key === '_index' ? 'sticky z-20 bg-gray-100 shadow-[2px_0_4px_-1px_rgba(0,0,0,0.08)]' : ''} ${'sortable' in col && col.sortable ? 'cursor-pointer select-none' : ''}`}
                        onClick={() => {
                          if (justResizedRef.current) {
                            justResizedRef.current = false;
                            return;
                          }

                          if ('sortable' in col && col.sortable) {
                            handleSort(col.key);
                          }
                        }}
                        onMouseDown={e => {
                          if (col.key === '_index') return;
                          if (isResizeEdgeHit(e.currentTarget, e.clientX)) {
                            startResize(col.key, e);
                          }
                        }}
                        onMouseEnter={() => col.key !== '_index' && setHoveredCol(col.key)}
                        onMouseLeave={() => setHoveredCol(null)}
                      >
                        <span className="flex items-center overflow-hidden">
                          <span className="truncate whitespace-nowrap">{col.label}</span>
                          {'sortable' in col && col.sortable && <SortIcon col={col.key} />}
                        </span>
                        {col.key !== '_index' && (
                          <div
                            role="separator"
                            aria-orientation="vertical"
                            aria-label={`调整${col.label}列宽`}
                            className="absolute right-0 top-0 z-30 flex h-full w-4 translate-x-1/2 items-center justify-center cursor-col-resize"
                            onMouseDown={e => startResize(col.key, e)}
                            onMouseEnter={() => setHoveredCol(col.key)}
                          >
                            <div className={`h-4 w-0.5 rounded-full transition-colors ${(hoveredCol === col.key || resizingCol === col.key) ? 'bg-primary' : 'bg-primary/0 hover:bg-primary/60'}`} />
                          </div>
                        )}
                      </TableHead>
                    ))}
                    {/* 弹性占位列，吸收剩余宽度，防止 _actions 被撑宽 */}
                    <TableHead style={{ width: 'auto' }} />
                    <TableHead
                      style={{ width: colWidths['_actions'], right: 0 }}
                      className="text-xs font-semibold sticky right-0 z-20 bg-gray-100 shadow-[-2px_0_4px_-1px_rgba(0,0,0,0.08)] text-center"
                    >
                      <span className="flex items-center justify-center">
                        <span className="truncate whitespace-nowrap">操作</span>
                      </span>
                    </TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {paged.map((row, i) => {
                    const expanded = expandedIds.has(row.id);
                    const stickyCellBgClass = 'bg-white group-hover:bg-blue-100';
                    const isDeleting = deleteMutation.isPending && deleteMutation.variables?.id === row.id;
                    return [
                      <TableRow
                        key={row.id}
                        className={`group ${i % 2 === 1 ? 'bg-muted/20' : ''} hover:bg-blue-100 transition-colors`}
                      >
                        <TableCell style={{ left: 0 }} className={`w-10 sticky z-[1] ${stickyCellBgClass}`} onClick={e => e.stopPropagation()}>
                          <Checkbox
                            checked={selectedKeys.has(row.id)}
                            onCheckedChange={checked => {
                              const next = new Set(selectedKeys);
                              if (checked) {
                                next.add(row.id);
                              } else {
                                next.delete(row.id);
                              }
                              setSelectedKeys(next);
                            }}
                            aria-label="选择行"
                          />
                        </TableCell>
                        <TableCell style={{ left: 40 }} className={`w-14 text-muted-foreground text-sm text-center sticky z-[1] ${stickyCellBgClass} shadow-[2px_0_4px_-1px_rgba(0,0,0,0.08)] whitespace-nowrap`}>{(safePage - 1) * pageSize + i + 1}</TableCell>
                        {visibleCOLS
                          .filter(col => col.key !== '_index' && col.key !== '_actions')
                          .map(col => renderDataCell(row, col.key))}
                        {/* 对应 header 弹性占位列 */}
                        <TableCell style={{ padding: 0 }} />
                        <TableCell style={{ width: colWidths._actions, minWidth: colWidths._actions }} className={`sticky right-0 z-[2] ${stickyCellBgClass} transition-all duration-300 ease-in-out shadow-[-2px_0_4px_-1px_rgba(0,0,0,0.08)] group-hover:shadow-[-8px_0_20px_rgba(0,0,0,0.14)] group-hover:z-[3]`}>
                          <div className="flex items-center gap-0.5">
                            <Button
                              variant="ghost"
                              size="sm"
                              className="text-xs text-blue-500 hover:text-blue-700"
                              onClick={e => toggleExpand(row.id, e)}
                            >
                              详情
                            </Button>
                            <Button
                              variant="ghost"
                              size="icon"
                              className="h-8 w-8 text-blue-500 hover:text-blue-700"
                              onClick={e => { e.stopPropagation(); openEditDialog(row); }}
                              aria-label={`编辑 ${row.material_name}`}
                            >
                              <Pencil className="h-4 w-4" />
                            </Button>
                            <Button
                              variant="ghost"
                              size="icon"
                              className="h-8 w-8 text-blue-500 hover:text-destructive"
                              onClick={e => {
                                e.stopPropagation();
                                const confirmMsg = `确认删除材料"${row.material_name}"吗？`;
                                if (!window.confirm(confirmMsg)) return;
                                deleteMutation.mutate(row);
                              }}
                              disabled={isDeleting}
                              aria-label={`删除 ${row.material_name}`}
                            >
                              {isDeleting ? <LoaderCircle className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
                            </Button>
                          </div>
                        </TableCell>
                      </TableRow>,
                      expanded && (
                        <TableRow key={`${row.id}-detail`} className="hover:bg-transparent">
                          <TableCell colSpan={visibleCOLS.length + 2} className="p-0 border-t">
                            <MaterialDetailPanel row={row} />
                          </TableCell>
                        </TableRow>
                      ),
                    ];
                  })}
                </TableBody>
              </Table>
            </div>
          )}

          {/* 分页 */}
          {filtered.length > 0 && (
            <div className="shrink-0 flex items-center justify-between mt-3 text-sm text-muted-foreground">
              <div className="flex items-center gap-2">
                <span>每页</span>
                <Select value={String(pageSize)} onValueChange={v => { setPageSize(Number(v)); setPage(1); }}>
                  <SelectTrigger className="w-16 h-7 text-xs">
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    {PAGE_SIZE_OPTIONS.map(n => (
                      <SelectItem key={n} value={String(n)}>{n}</SelectItem>
                    ))}
                  </SelectContent>
                </Select>
                <span>条，共 {filtered.length} 条</span>
              </div>
              <div className="flex items-center gap-1">
                <Button
                  variant="ghost" size="icon" className="h-7 w-7"
                  disabled={safePage <= 1}
                  onClick={() => setPage(p => p - 1)}
                >
                  <ChevronLeft className="h-4 w-4" />
                </Button>
                <span className="px-2">{safePage} / {totalPages}</span>
                <Button
                  variant="ghost" size="icon" className="h-7 w-7"
                  disabled={safePage >= totalPages}
                  onClick={() => setPage(p => p + 1)}
                >
                  <ChevronRight className="h-4 w-4" />
                </Button>
              </div>
            </div>
          )}
        </div>
      )}
      {/* AI 标准化确认框 */}
      <StandardizeConfirmDialog
        open={standardizeConfirm.open}
        onOpenChange={open => setStandardizeConfirm(prev => ({ ...prev, open }))}
        totalRows={standardizeConfirm.totalRows}
        executionMode={standardizeConfirm.executionMode}
        onConfirm={executeStandardize}
      />

      <ColumnConfigDialog
        open={configOpen}
        onOpenChange={setConfigOpen}
        columns={COLS}
        visibleCols={visibleCols}
        orderedKeys={orderedColKeys}
        onConfirm={({ visibleCols: nextVisibleCols, orderedKeys: nextOrderedKeys }) => {
          setColumnConfig({ visibleCols: nextVisibleCols, orderedKeys: nextOrderedKeys });
        }}
      />
      <SourceFileViewDialog batchId={fileDialogBatchId} open={!!fileDialogBatchId} onClose={() => setFileDialogBatchId(null)} />
      <ExportDialog
        open={exportOpen}
        onOpenChange={setExportOpen}
        data={exportData as unknown as Record<string, unknown>[]}
        totalCount={filtered.length}
        filename={`采购清单_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.xlsx`}
        columns={[
          { key: 'batch_id', label: '批次ID', group: '基础字段' },
          { key: 'material_name', label: '材料名称', group: '基础字段' },
          { key: 'specification', label: '规格型号', group: '基础字段' },
          { key: 'quantity', label: '数量', group: '基础字段' },
          { key: 'unit', label: '单位', group: '基础字段' },
          { key: 'brand', label: '品牌', group: '基础字段' },
          { key: 'source_file', label: '来源文件', group: '基础字段' },
          { key: 'created_by', label: '创建人', group: '基础字段' },
          { key: 'created_at', label: '创建时间', group: '基础字段' },
          { key: 'technical_standard', label: '技术标准', group: '详情字段', defaultChecked: false },
          { key: 'material_requirement', label: '材质要求', group: '详情字段', defaultChecked: false },
          { key: 'technical_requirement', label: '技术要求', group: '详情字段', defaultChecked: false },
          { key: 'applicable_conditions', label: '适用条件', group: '详情字段', defaultChecked: false },
          { key: 'test_requirement', label: '试验测试要求', group: '详情字段', defaultChecked: false },
          { key: 'acceptance_note', label: '验收说明', group: '详情字段', defaultChecked: false },
          { key: 'warranty_period', label: '质保期', group: '详情字段', defaultChecked: false },
          { key: 'deadline', label: '交货期限', group: '详情字段', defaultChecked: false },
          { key: 'invoice_type', label: '发票种类', group: '详情字段', defaultChecked: false },
          { key: 'remarks', label: '备注', group: '详情字段', defaultChecked: false },
          { key: 'delivery_address', label: '收货地址', group: '详情字段', defaultChecked: false },
        ]}
      />

      {/* 编辑材料弹窗 */}
      <MaterialEditDialog
        editingMaterial={editingMaterial}
        editForm={editForm}
        onEditFormChange={setEditForm}
        onClose={() => setEditingMaterial(null)}
        onSave={handleSaveEdit}
        isSaving={updateMutation.isPending}
      />
    </div>
  );
}
