import { useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { Download, ExternalLink, LayoutGrid, List, LoaderCircle, MessageCircle, RefreshCw, Search, SlidersHorizontal, Trash2 } from 'lucide-react';

import { ColumnConfigDialog } from '@/components/shared/ColumnConfigDialog';
import { DataTable, Column } from '@/components/shared/DataTable';
import { ExportDialog } from '@/components/shared/ExportDialog';
import { PageHeader } from '@/components/shared/PageHeader';
import { SearchResultsDirectoryTree } from '@/components/shared/SearchResultsDirectoryTree';
import { SearchTaskStatusPanel } from '@/components/shared/SearchTaskStatusPanel';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Skeleton } from '@/components/ui/skeleton';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { useProcurementBatchOptions } from '@/hooks/useProcurementBatchOptions';
import { formatDate } from '@/lib/dateUtils';
import { getStoredColumnConfig, resolveStoredColumnConfig, saveStoredColumnConfig } from '@/lib/columnConfigStorage';
import { fetchContactLogs } from '@/services/contactLogService';
import { fetchMaterials } from '@/services/materialService';
import { fetchMerchantContacts } from '@/services/merchantService';
import { fetchSearchResults, fetchSearchStageSummaries, deleteSearchResultsByIds, deleteSearchStageGroup } from '@/services/searchService';
import { fetchStandardized } from '@/services/standardizedService';
import { ContactLog, MerchantContact, ProcurementMaterial, SearchResult, SearchStageSummary, StandardizedMaterial } from '@/types';
import { formatStageSkipReason, getPlaceholderMessage, getShortageMessage } from '@/pages/searchResultsMessages';

type DisplaySearchResult = SearchResult & {
  material_name: string;
  standardized_name: string;
  standardized_search_term: string;
};

type StageGroup = {
  groupKey: string;
  task_id: string | null;
  batch_id: string;
  material_id: string;
  stage_order: number | null;
  stage_label: string;
  stage_kind: string;
  stage_main_keyword: string;
  keyword_hit_rate: number | null;
  material_name: string;
  standardized_name: string;
  standardized_search_term: string;
  results: DisplaySearchResult[];
  merchant_count: number;
  shortage_reason: string;
  has_placeholder: boolean;
  placeholder_reason: string;
  contact_enrichment_skip_reason: string;
  created_at: string;
};

type KeywordSection = {
  directoryKey: string;
  anchorId: string;
  batch_id: string;
  task_id: string | null;
  keyword: string;
  stageGroups: StageGroup[];
  material_names: string[];
  merchant_count: number;
  latest_created_at: string;
};

type TaskSection = {
  directoryKey: string;
  anchorId: string;
  batch_id: string;
  task_id: string | null;
  task_label: string;
  keywordSections: KeywordSection[];
  material_names: string[];
  merchant_count: number;
  latest_created_at: string;
};

type BatchSection = {
  directoryKey: string;
  anchorId: string;
  batch_id: string;
  taskSections: TaskSection[];
  merchant_count: number;
  latest_created_at: string;
};

type SearchResultsTabValue = 'results' | 'tasks';

const SEARCH_RESULT_COLUMN_OPTIONS = [
  { key: 'batch_id', label: '批次ID' },
  { key: 'material_name', label: '材料名称' },
  { key: 'standardized_name', label: '标准化名称' },
  { key: 'title', label: '商品标题' },
  { key: 'keyword', label: '搜索关键词' },
  { key: 'score', label: '标题分数' },
  { key: 'is_match', label: 'LLM判定' },
  { key: 'reason', label: '判定原因' },
  { key: 'merchant_name', label: '商家名称' },
  { key: 'phone', label: '电话' },
  { key: 'mobile', label: '手机' },
  { key: 'created_at', label: '时间' },
] as const;

const SEARCH_RESULT_STORAGE_KEY = 'autoprocurement.search-results.columns';
const SEARCH_RESULT_COLUMN_KEYS = SEARCH_RESULT_COLUMN_OPTIONS.map((column) => column.key);
const UPGRADED_DEFAULT_VISIBLE_KEYS = ['score', 'is_match', 'reason'] as const;
const LEGACY_TASK_ID = '__legacy__';
const LEGACY_TASK_LABEL = '历史结果';

function normalizeSearchResultColumnOrder(orderedKeys: string[]) {
  const next = orderedKeys.filter((key) => key !== 'material_name' && key !== 'standardized_name' && key !== 'keyword_hit_rate');
  const batchIndex = next.indexOf('batch_id');

  if (batchIndex === -1) {
    return ['batch_id', 'material_name', 'standardized_name', ...next.filter((key) => key !== 'batch_id')];
  }

  next.splice(batchIndex + 1, 0, 'material_name', 'standardized_name');
  return [...new Set(next)];
}

function getInitialSearchResultColumnConfig() {
  const storedConfig = getStoredColumnConfig(SEARCH_RESULT_STORAGE_KEY);
  const config = resolveStoredColumnConfig({
    storageKey: SEARCH_RESULT_STORAGE_KEY,
    allKeys: SEARCH_RESULT_COLUMN_KEYS,
    defaultVisibleKeys: SEARCH_RESULT_COLUMN_KEYS,
  });

  const visibleCols = new Set(config.visibleCols);
  UPGRADED_DEFAULT_VISIBLE_KEYS.forEach((key) => {
    if (!storedConfig?.orderedKeys.includes(key)) {
      visibleCols.add(key);
    }
  });

  return {
    ...config,
    visibleCols,
    orderedKeys: normalizeSearchResultColumnOrder(config.orderedKeys),
  };
}

function buildStageGroupKey(batchId: string, taskId: string | null | undefined, materialId: string | undefined, stageOrder: number | null | undefined, fallback: string) {
  const taskKey = taskId?.trim() || LEGACY_TASK_ID;
  const materialKey = materialId || fallback || 'unknown';

  if (stageOrder != null) {
    return `${batchId}::${taskKey}::${materialKey}::${stageOrder}`;
  }

  return `${batchId}::${taskKey}::${materialKey}::legacy`;
}

function buildBatchMaterialLookupKey(batchId: string, materialId: string) {
  return `${batchId}::${materialId}`;
}

function normalizeTaskId(taskId: string | null | undefined) {
  const trimmed = taskId?.trim() ?? '';
  return trimmed || null;
}

function getTaskDirectoryKey(batchId: string, taskId: string | null | undefined) {
  return `${batchId}::${normalizeTaskId(taskId) || LEGACY_TASK_ID}`;
}

function getTaskLabel(taskId: string | null | undefined) {
  return normalizeTaskId(taskId) || LEGACY_TASK_LABEL;
}

function buildAnchorId(...parts: Array<string | number | null | undefined>) {
  return parts.map((part) => encodeURIComponent(String(part ?? ''))).join('--');
}

function formatHitRate(rate: number | null | undefined) {
  if (rate == null) {
    return '-';
  }

  return `${(rate * 100).toFixed(1)}%`;
}

function formatScore(score: number | null | undefined) {
  if (score == null) {
    return '-';
  }

  return `${Math.round(score)}分`;
}

function getMatchStatusMeta(value: boolean | null | undefined) {
  if (value === true) {
    return {
      label: '匹配',
      className: 'bg-green-50 text-green-700',
    };
  }

  if (value === false) {
    return {
      label: '不匹配',
      className: 'bg-red-50 text-red-600',
    };
  }

  return {
    label: '未判定',
    className: 'bg-muted text-muted-foreground',
  };
}

function getMatchReasonText(result: Pick<SearchResult, 'is_placeholder' | 'reason' | 'is_match'>) {
  if (result.is_placeholder) {
    return '';
  }

  if (result.reason) {
    return result.reason;
  }

  if (result.is_match == null) {
    return '该结果未执行 LLM 语义判定。';
  }

  return 'LLM 未返回判定原因。';
}

function pickGroupSelectedKeys(selectedKeys: Set<string>, rows: DisplaySearchResult[]) {
  return new Set(rows.map((row) => row.id).filter((id) => selectedKeys.has(id)));
}

function mergeGroupSelectedKeys(currentKeys: Set<string>, rows: DisplaySearchResult[], nextKeys: Set<string>) {
  const rowKeys = new Set(rows.map((row) => row.id));
  const merged = new Set([...currentKeys].filter((key) => !rowKeys.has(key)));
  nextKeys.forEach((key) => merged.add(key));
  return merged;
}

function renderGroupHeader(group: StageGroup, options?: {
  showDeleteAction?: boolean;
  deleting?: boolean;
  disableDeleteAction?: boolean;
  onDelete?: () => void;
}) {
  const placeholderMessage = group.has_placeholder ? getPlaceholderMessage(group.placeholder_reason) : '';
  const skipReason = formatStageSkipReason(group.contact_enrichment_skip_reason);
  const showSkipReason = Boolean(skipReason) && skipReason !== placeholderMessage;

  return (
    <div className="rounded-lg border border-border bg-muted/20 p-3">
      <div className="flex flex-wrap items-center gap-3">
        <div>
          <div className="text-sm font-medium text-foreground">{group.standardized_search_term || group.standardized_name || group.material_name}</div>
          <div className="text-xs text-muted-foreground">关键词搜索（AI 自动多轮）</div>
        </div>
        <div className="ml-auto flex flex-wrap items-center gap-2 text-xs">
          <span className="rounded-full bg-background px-2 py-1 text-muted-foreground">批次 {group.batch_id}</span>
          <span className="rounded-full bg-background px-2 py-1 text-muted-foreground">命中率 {formatHitRate(group.keyword_hit_rate)}</span>
          {group.shortage_reason ? (
            <span className="rounded-full bg-background px-2 py-1 text-muted-foreground">缺口原因 {getShortageMessage(group.shortage_reason)}</span>
          ) : null}
          {group.has_placeholder && (
            <span className="rounded-full bg-red-50 px-2 py-1 text-red-600">{placeholderMessage}</span>
          )}
          {showSkipReason && (
            <span className="rounded-full bg-amber-50 px-2 py-1 text-amber-700">跳过原因: {skipReason}</span>
          )}
          {options?.showDeleteAction && (
            <Button
              variant="ghost"
              size="sm"
              className="h-7 px-2 text-xs text-destructive hover:text-destructive"
              onClick={(event) => {
                event.stopPropagation();
                options.onDelete?.();
              }}
              disabled={options.deleting || options.disableDeleteAction}
            >
              {options.deleting ? <LoaderCircle className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Trash2 className="mr-1 h-3.5 w-3.5" />}
              删除
            </Button>
          )}
        </div>
      </div>
    </div>
  );
}

export default function SearchResults() {
  const [searchParams, setSearchParams] = useSearchParams();
  const [loading, setLoading] = useState(true);
  const [view, setView] = useState<'list' | 'card'>('list');
  const [deleting, setDeleting] = useState(false);
  const [deletingGroupKey, setDeletingGroupKey] = useState<string | null>(null);
  const [selected, setSelected] = useState<DisplaySearchResult | null>(null);
  const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
  const [exportOpen, setExportOpen] = useState(false);
  const [configOpen, setConfigOpen] = useState(false);
  const [columnConfig, setColumnConfig] = useState(() => getInitialSearchResultColumnConfig());
  const [openDirectoryKeys, setOpenDirectoryKeys] = useState<Set<string>>(new Set());
  const [selectedKeywordDirectoryKey, setSelectedKeywordDirectoryKey] = useState<string | null>(null);
  const { visibleCols, orderedKeys: orderedColKeys } = columnConfig;

  const [allSearchResults, setAllSearchResults] = useState<SearchResult[]>([]);
  const [allStageSummaries, setAllStageSummaries] = useState<SearchStageSummary[]>([]);
  const [allMerchantContacts, setAllMerchantContacts] = useState<MerchantContact[]>([]);
  const [allMaterials, setAllMaterials] = useState<ProcurementMaterial[]>([]);
  const [allStandardized, setAllStandardized] = useState<StandardizedMaterial[]>([]);
  const [allContactLogs, setAllContactLogs] = useState<ContactLog[]>([]);
  const { batchOptions } = useProcurementBatchOptions();
  const activeTabParam = searchParams.get('tab');
  const activeTab: SearchResultsTabValue = activeTabParam === 'tasks' ? 'tasks' : 'results';
  const taskBatchId = searchParams.get('batch_id') ?? '';

  const loadData = async () => {
    const [searchResults, stageSummaries, merchantContacts, materials, standardized, contactLogs] = await Promise.all([
      fetchSearchResults({ includePlaceholders: true }),
      fetchSearchStageSummaries(),
      fetchMerchantContacts(),
      fetchMaterials(),
      fetchStandardized(),
      fetchContactLogs(),
    ]);

    setAllSearchResults(searchResults);
    setAllStageSummaries(stageSummaries);
    setAllMerchantContacts(merchantContacts);
    setAllMaterials(materials);
    setAllStandardized(standardized);
    setAllContactLogs(contactLogs);
  };

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        await loadData();
      } catch (err) {
        console.error('SearchResults data load error:', err);
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    })();

    return () => {
      cancelled = true;
    };
  }, []);

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

  const merchantPhoneLookup = useMemo(() => {
    const byContactPage = new Map<string, string>();
    const byMerchantUrl = new Map<string, string>();
    const byMerchantName = new Map<string, string>();

    allMerchantContacts.forEach((contact) => {
      if (contact.contact_page_url && contact.phone) {
        byContactPage.set(contact.contact_page_url, contact.phone);
      }
      if (contact.merchant_url && contact.phone) {
        byMerchantUrl.set(contact.merchant_url, contact.phone);
      }
      if (contact.merchant_name && contact.phone) {
        byMerchantName.set(contact.merchant_name, contact.phone);
      }
    });

    return { byContactPage, byMerchantUrl, byMerchantName };
  }, [allMerchantContacts]);

  const materialLookup = useMemo(() => {
    const byMaterialId = new Map<string, string>();
    allMaterials.forEach((material) => {
      byMaterialId.set(buildBatchMaterialLookupKey(material.batch_id, material.id), material.material_name);
    });
    return byMaterialId;
  }, [allMaterials]);

  const standardizedLookup = useMemo(() => {
    const byMaterialId = new Map<string, string>();
    allStandardized.forEach((material) => {
      byMaterialId.set(buildBatchMaterialLookupKey(material.batch_id, material.material_id), material.standardized_name);
    });
    return byMaterialId;
  }, [allStandardized]);

  const standardizedSearchTermLookup = useMemo(() => {
    const byMaterialId = new Map<string, string>();
    allStandardized.forEach((material) => {
      const searchTerm = material.primary_search_term?.trim() || material.standardized_name?.trim() || '';
      byMaterialId.set(buildBatchMaterialLookupKey(material.batch_id, material.material_id), searchTerm);
    });
    return byMaterialId;
  }, [allStandardized]);

  const materialOrderLookup = useMemo(() => {
    const byMaterialId = new Map<string, number>();
    const batchOrder = new Map<string, number>();

    allMaterials.forEach((material) => {
      const nextOrder = (batchOrder.get(material.batch_id) ?? 0) + 1;
      batchOrder.set(material.batch_id, nextOrder);
      byMaterialId.set(buildBatchMaterialLookupKey(material.batch_id, material.id), nextOrder);
    });

    return byMaterialId;
  }, [allMaterials]);

  const displayResults = useMemo<DisplaySearchResult[]>(() => allSearchResults.map((result) => ({
    ...result,
    material_name: result.material_name || materialLookup.get(buildBatchMaterialLookupKey(result.batch_id, result.material_id)) || '-',
    standardized_name: result.standardized_name || standardizedLookup.get(buildBatchMaterialLookupKey(result.batch_id, result.material_id)) || '-',
    standardized_search_term: standardizedSearchTermLookup.get(buildBatchMaterialLookupKey(result.batch_id, result.material_id))
      || result.standardized_name
      || result.material_name
      || '-',
    phone: result.is_placeholder
      ? ''
      : (
        merchantPhoneLookup.byContactPage.get(result.contact_page_url) ||
        merchantPhoneLookup.byMerchantUrl.get(result.merchant_url) ||
        merchantPhoneLookup.byMerchantName.get(result.merchant_name) ||
        result.phone
      ),
  })), [allSearchResults, materialLookup, merchantPhoneLookup, standardizedLookup, standardizedSearchTermLookup]);

  const stageGroups = useMemo<StageGroup[]>(() => {
    const groups = new Map<string, StageGroup>();
    const resolveMaterialName = (currentBatchId: string, currentMaterialId: string) => (
      materialLookup.get(buildBatchMaterialLookupKey(currentBatchId, currentMaterialId)) || '-'
    );
    const resolveStandardizedName = (currentBatchId: string, currentMaterialId: string) => (
      standardizedLookup.get(buildBatchMaterialLookupKey(currentBatchId, currentMaterialId)) || '-'
    );
    const resolveStandardizedSearchTerm = (currentBatchId: string, currentMaterialId: string) => (
      standardizedSearchTermLookup.get(buildBatchMaterialLookupKey(currentBatchId, currentMaterialId))
      || resolveStandardizedName(currentBatchId, currentMaterialId)
      || resolveMaterialName(currentBatchId, currentMaterialId)
      || '-'
    );
    const getMaterialOrder = (currentBatchId: string, currentMaterialId: string) => (
      materialOrderLookup.get(buildBatchMaterialLookupKey(currentBatchId, currentMaterialId)) ?? Number.MAX_SAFE_INTEGER
    );

    const ensureGroup = (seed: {
      task_id?: string | null;
      batch_id: string;
      material_id: string;
      stage_order: number | null;
      stage_label: string;
      stage_kind: string;
      stage_main_keyword: string;
      keyword_hit_rate: number | null;
      shortage_reason?: string;
      contact_enrichment_skip_reason?: string;
      created_at: string;
      fallbackKey: string;
    }) => {
      const groupKey = buildStageGroupKey(seed.batch_id, seed.task_id, seed.material_id, seed.stage_order, seed.fallbackKey);
      const existing = groups.get(groupKey);
      if (existing) {
        return existing;
      }

      const group: StageGroup = {
        groupKey,
        task_id: normalizeTaskId(seed.task_id),
        batch_id: seed.batch_id,
        material_id: seed.material_id,
        stage_order: seed.stage_order,
        stage_label: seed.stage_label,
        stage_kind: seed.stage_kind,
        stage_main_keyword: seed.stage_main_keyword,
        keyword_hit_rate: seed.keyword_hit_rate,
        material_name: resolveMaterialName(seed.batch_id, seed.material_id) || `物料 ${seed.material_id}`,
        standardized_name: resolveStandardizedName(seed.batch_id, seed.material_id),
        standardized_search_term: resolveStandardizedSearchTerm(seed.batch_id, seed.material_id),
        results: [],
        merchant_count: 0,
        shortage_reason: seed.shortage_reason || '',
        has_placeholder: false,
        placeholder_reason: '',
        contact_enrichment_skip_reason: seed.contact_enrichment_skip_reason || '',
        created_at: seed.created_at,
      };
      groups.set(groupKey, group);
      return group;
    };

    for (const stage of allStageSummaries) {
      ensureGroup({
        task_id: stage.task_id,
        batch_id: stage.batch_id,
        material_id: stage.material_id,
        stage_order: stage.stage_order,
        stage_label: stage.stage_label,
        stage_kind: stage.stage_kind,
        stage_main_keyword: stage.stage_main_keyword,
        keyword_hit_rate: stage.keyword_hit_rate ?? null,
        shortage_reason: stage.shortage_reason || '',
        contact_enrichment_skip_reason: stage.contact_enrichment_skip_reason,
        created_at: stage.created_at,
        fallbackKey: `${stage.stage_label}-${stage.stage_order}`,
      });
    }

    for (const result of displayResults) {
      const group = ensureGroup({
        task_id: result.task_id,
        batch_id: result.batch_id,
        material_id: result.material_id,
        stage_order: result.stage_order ?? null,
        stage_label: result.stage_label || '历史结果',
        stage_kind: result.stage_kind || 'legacy',
        stage_main_keyword: result.stage_main_keyword || result.keyword,
        keyword_hit_rate: result.stage_keyword_hit_rate ?? result.keyword_hit_rate ?? null,
        shortage_reason: '',
        created_at: result.created_at,
        fallbackKey: result.id,
      });

      group.results.push(result);
      if (result.is_placeholder) {
        group.has_placeholder = true;
        group.placeholder_reason = result.placeholder_reason || group.placeholder_reason;
      } else {
        group.merchant_count += 1;
      }
    }

    return [...groups.values()]
      .map((group) => ({
        ...group,
        results: [...group.results].sort((left, right) => {
          if (Boolean(left.is_placeholder) !== Boolean(right.is_placeholder)) {
            return left.is_placeholder ? 1 : -1;
          }
          return (left.result_index ?? 0) - (right.result_index ?? 0);
        }),
      }))
      .sort((left, right) => {
        if (left.batch_id !== right.batch_id) {
          return right.batch_id.localeCompare(left.batch_id);
        }
        const leftTaskKey = left.task_id || LEGACY_TASK_ID;
        const rightTaskKey = right.task_id || LEGACY_TASK_ID;
        if (leftTaskKey !== rightTaskKey) {
          if (leftTaskKey === LEGACY_TASK_ID) {
            return 1;
          }
          if (rightTaskKey === LEGACY_TASK_ID) {
            return -1;
          }
          return right.created_at.localeCompare(left.created_at) || leftTaskKey.localeCompare(rightTaskKey);
        }
        const leftOrder = getMaterialOrder(left.batch_id, left.material_id);
        const rightOrder = getMaterialOrder(right.batch_id, right.material_id);
        if (leftOrder !== rightOrder) {
          return leftOrder - rightOrder;
        }
        if (left.material_id !== right.material_id) {
          return left.material_id.localeCompare(right.material_id);
        }
        if ((left.stage_order ?? 0) !== (right.stage_order ?? 0)) {
          return (left.stage_order ?? 0) - (right.stage_order ?? 0);
        }
        return right.created_at.localeCompare(left.created_at);
      });
  }, [allStageSummaries, displayResults, materialLookup, materialOrderLookup, standardizedLookup, standardizedSearchTermLookup]);

  const batchSections = useMemo<BatchSection[]>(() => {
    const batchMap = new Map<string, {
      section: Omit<BatchSection, 'taskSections'>;
      taskMap: Map<string, {
        section: Omit<TaskSection, 'keywordSections' | 'material_names'>;
        keywordMap: Map<string, {
          section: Omit<KeywordSection, 'stageGroups' | 'material_names'>;
          stageGroups: StageGroup[];
          materialNames: Set<string>;
        }>;
        materialNames: Set<string>;
      }>;
    }>();

    for (const group of stageGroups) {
      const batchEntry = batchMap.get(group.batch_id) ?? (() => {
        const next = {
          section: {
            directoryKey: group.batch_id,
            anchorId: buildAnchorId('batch', group.batch_id),
            batch_id: group.batch_id,
            merchant_count: 0,
            latest_created_at: group.created_at,
          },
          taskMap: new Map<string, {
            section: Omit<TaskSection, 'keywordSections' | 'material_names'>;
            keywordMap: Map<string, {
              section: Omit<KeywordSection, 'stageGroups' | 'material_names'>;
              stageGroups: StageGroup[];
              materialNames: Set<string>;
            }>;
            materialNames: Set<string>;
          }>(),
        };
        batchMap.set(group.batch_id, next);
        return next;
      })();

      batchEntry.section.merchant_count += group.merchant_count;
      if (group.created_at > batchEntry.section.latest_created_at) {
        batchEntry.section.latest_created_at = group.created_at;
      }

      const taskDirectoryKey = getTaskDirectoryKey(group.batch_id, group.task_id);
      const taskEntry = batchEntry.taskMap.get(taskDirectoryKey) ?? (() => {
        const next = {
          section: {
            directoryKey: taskDirectoryKey,
            anchorId: buildAnchorId('task', group.batch_id, group.task_id || LEGACY_TASK_ID),
            batch_id: group.batch_id,
            task_id: group.task_id,
            task_label: getTaskLabel(group.task_id),
            merchant_count: 0,
            latest_created_at: group.created_at,
          },
          keywordMap: new Map<string, {
            section: Omit<KeywordSection, 'stageGroups' | 'material_names'>;
            stageGroups: StageGroup[];
            materialNames: Set<string>;
          }>(),
          materialNames: new Set<string>(),
        };
        batchEntry.taskMap.set(taskDirectoryKey, next);
        return next;
      })();

      taskEntry.section.merchant_count += group.merchant_count;
      if (group.created_at > taskEntry.section.latest_created_at) {
        taskEntry.section.latest_created_at = group.created_at;
      }
      taskEntry.materialNames.add(group.material_name);

      const keywordLabel = group.standardized_search_term || group.standardized_name || group.material_name || '-';
      const keywordDirectoryKey = `${taskDirectoryKey}::${group.material_id}`;
      const keywordEntry = taskEntry.keywordMap.get(keywordDirectoryKey) ?? (() => {
        const next = {
          section: {
            directoryKey: keywordDirectoryKey,
            anchorId: buildAnchorId('standardized-search-term', group.batch_id, group.task_id || LEGACY_TASK_ID, group.material_id),
            batch_id: group.batch_id,
            task_id: group.task_id,
            keyword: keywordLabel,
            merchant_count: 0,
            latest_created_at: group.created_at,
          },
          stageGroups: [] as StageGroup[],
          materialNames: new Set<string>(),
        };
        taskEntry.keywordMap.set(keywordDirectoryKey, next);
        return next;
      })();

      keywordEntry.section.merchant_count += group.merchant_count;
      if (group.created_at > keywordEntry.section.latest_created_at) {
        keywordEntry.section.latest_created_at = group.created_at;
      }
      keywordEntry.materialNames.add(group.material_name);
      keywordEntry.stageGroups.push(group);
    }

    return [...batchMap.values()]
      .map((batchEntry) => ({
        ...batchEntry.section,
        taskSections: [...batchEntry.taskMap.values()]
          .map((taskEntry) => ({
            ...taskEntry.section,
            material_names: [...taskEntry.materialNames],
            keywordSections: [...taskEntry.keywordMap.values()]
              .map((keywordEntry) => ({
                ...keywordEntry.section,
                material_names: [...keywordEntry.materialNames],
                stageGroups: [...keywordEntry.stageGroups].sort((left, right) => {
                  const leftOrder = materialOrderLookup.get(buildBatchMaterialLookupKey(left.batch_id, left.material_id)) ?? Number.MAX_SAFE_INTEGER;
                  const rightOrder = materialOrderLookup.get(buildBatchMaterialLookupKey(right.batch_id, right.material_id)) ?? Number.MAX_SAFE_INTEGER;
                  if (leftOrder !== rightOrder) {
                    return leftOrder - rightOrder;
                  }
                  if ((left.stage_order ?? 0) !== (right.stage_order ?? 0)) {
                    return (left.stage_order ?? 0) - (right.stage_order ?? 0);
                  }
                  return right.created_at.localeCompare(left.created_at);
                }),
              }))
              .sort((left, right) => right.latest_created_at.localeCompare(left.latest_created_at) || left.keyword.localeCompare(right.keyword)),
          }))
          .sort((left, right) => {
            if (left.task_id && !right.task_id) {
              return -1;
            }
            if (!left.task_id && right.task_id) {
              return 1;
            }
            return right.latest_created_at.localeCompare(left.latest_created_at) || left.task_label.localeCompare(right.task_label);
          }),
      }))
      .sort((left, right) => right.batch_id.localeCompare(left.batch_id));
  }, [materialOrderLookup, stageGroups]);

  const keywordSectionLookup = useMemo(() => {
    const next = new Map<string, KeywordSection>();

    batchSections.forEach((batchSection) => {
      batchSection.taskSections.forEach((taskSection) => {
        taskSection.keywordSections.forEach((keywordSection) => {
          next.set(keywordSection.directoryKey, keywordSection);
        });
      });
    });

    return next;
  }, [batchSections]);

  const selectedKeywordSection = useMemo(
    () => (selectedKeywordDirectoryKey ? keywordSectionLookup.get(selectedKeywordDirectoryKey) ?? null : null),
    [keywordSectionLookup, selectedKeywordDirectoryKey],
  );

  const visibleStageGroups = useMemo<StageGroup[]>(() => {
    if (!selectedKeywordSection) {
      return [];
    }

    return selectedKeywordSection.stageGroups;
  }, [selectedKeywordSection]);

  const visibleResults = useMemo(
    () => visibleStageGroups.flatMap((group) => group.results),
    [visibleStageGroups],
  );

  useEffect(() => {
    setOpenDirectoryKeys(new Set([
      ...batchSections.map((section) => section.directoryKey),
      ...batchSections.flatMap((section) => section.taskSections.map((taskSection) => taskSection.directoryKey)),
    ]));
  }, [batchSections]);

  useEffect(() => {
    setSelectedKeywordDirectoryKey((current) => {
      if (current && keywordSectionLookup.has(current)) {
        return current;
      }

      return null;
    });
  }, [keywordSectionLookup]);

  useEffect(() => {
    const visibleIds = new Set(visibleResults.map((result) => result.id));

    setSelectedKeys((current) => {
      if (current.size === 0) {
        return current;
      }

      const nextKeys = [...current].filter((key) => visibleIds.has(key));
      return nextKeys.length === current.size ? current : new Set(nextKeys);
    });

    setSelected((current) => {
      if (!current) {
        return current;
      }

      return visibleIds.has(current.id) ? current : null;
    });
  }, [visibleResults]);

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

  const handleTaskBatchIdChange = (nextBatchId: string) => {
    const nextParams = new URLSearchParams(searchParams);

    if (nextBatchId) {
      nextParams.set('batch_id', nextBatchId);
    } else {
      nextParams.delete('batch_id');
    }

    setSearchParams(nextParams, { replace: true });
  };

  const handleRefresh = async () => {
    setLoading(true);
    try {
      await loadData();
      toast('搜索结果已刷新');
    } catch (err) {
      console.error('SearchResults refresh error:', err);
      toast('搜索结果刷新失败');
    } finally {
      setLoading(false);
    }
  };

  const handleBatchDelete = async () => {
    if (selectedKeys.size === 0) {
      return;
    }

    const ids = [...selectedKeys];
    if (!window.confirm(`确定要删除选中的 ${ids.length} 条搜索结果吗？关联的联系记录和询价记录也会一并清理，此操作不可撤销。`)) {
      return;
    }

    setDeleting(true);
    try {
      await deleteSearchResultsByIds(ids);
      await loadData();
      setSelectedKeys(new Set());
      setSelected((current) => (current && ids.includes(current.id) ? null : current));
      toast.success(`已删除 ${ids.length} 条搜索结果`);
    } catch (err) {
      console.error('SearchResults batch delete error:', err);
      toast.error(`删除失败：${err instanceof Error ? err.message : String(err)}`);
    } finally {
      setDeleting(false);
    }
  };

  const canDeleteStageGroup = (group: StageGroup) => group.merchant_count === 0 && Boolean(group.material_id) && group.stage_order != null;

  const handleGroupDelete = async (group: StageGroup) => {
    if (!canDeleteStageGroup(group)) {
      return;
    }

    const resultIds = new Set(group.results.map((result) => result.id));
    const groupName = group.material_name || `物料 ${group.material_id}`;
    if (!window.confirm(`确定要删除 ${groupName} 的${group.stage_label}记录吗？该分组下的搜索结果、联系记录和询价记录会一并清理，此操作不可撤销。`)) {
      return;
    }

    setDeletingGroupKey(group.groupKey);
    try {
      await deleteSearchStageGroup({
        batchId: group.batch_id,
        materialId: group.material_id,
        stageOrder: group.stage_order!,
      });
      await loadData();
      setSelectedKeys((current) => new Set([...current].filter((key) => !resultIds.has(key))));
      setSelected((current) => (current && resultIds.has(current.id) ? null : current));
      toast.success(`已删除 ${groupName} 的${group.stage_label}记录`);
    } catch (err) {
      console.error('SearchResults group delete error:', err);
      toast.error(`删除失败：${err instanceof Error ? err.message : String(err)}`);
    } finally {
      setDeletingGroupKey(null);
    }
  };

  const columns = useMemo<Column<DisplaySearchResult>[]>(() => [
    { key: 'batch_id', label: '批次ID', defaultWidth: 150, render: (value) => <span className="font-mono text-xs whitespace-nowrap">{String(value)}</span> },
    { key: 'material_name', label: '材料名称', defaultWidth: 90, nowrap: true },
    { key: 'standardized_name', label: '标准化名称', defaultWidth: 110, nowrap: true },
    {
      key: 'title',
      label: '商品标题',
      truncate: true,
      defaultWidth: 520,
      render: (value, row) => row.offer_url && !row.is_placeholder ? (
        <a href={row.offer_url} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline" onClick={(event) => event.stopPropagation()}>
          {String(value || '-')}
        </a>
      ) : (
        <span className={row.is_placeholder ? 'text-red-600 font-medium' : ''}>{String(value || '-')}</span>
      ),
    },
    { key: 'keyword', label: '搜索关键词', defaultWidth: 130 },
    {
      key: 'score',
      label: '标题分数',
      defaultWidth: 90,
      sortable: true,
      render: (value, row) => (
        <span className={row.is_placeholder ? 'text-muted-foreground' : 'font-medium text-foreground'}>{formatScore(value as number | null | undefined)}</span>
      ),
    },
    {
      key: 'is_match',
      label: 'LLM判定',
      defaultWidth: 96,
      render: (value, row) => {
        if (row.is_placeholder) {
          return <span className="text-muted-foreground">-</span>;
        }

        const meta = getMatchStatusMeta(value as boolean | null | undefined);
        return <span className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${meta.className}`}>{meta.label}</span>;
      },
    },
    {
      key: 'reason',
      label: '判定原因',
      defaultWidth: 260,
      truncate: true,
      render: (_, row) => (
        <span className="text-xs text-muted-foreground">{getMatchReasonText(row)}</span>
      ),
    },
    { key: 'merchant_name', label: '商家名称', defaultWidth: 180, nowrap: true },
    { key: 'phone', label: '电话', defaultWidth: 130, render: (value) => <span className="whitespace-nowrap">{String(value || '-')}</span> },
    { key: 'mobile', label: '手机', defaultWidth: 140, nowrap: true, render: (value) => String(value || '-') },
    { key: 'created_at', label: '时间', sortable: true, defaultWidth: 140, render: (value) => formatDate(value as string) },
    {
      key: '_actions',
      label: '操作',
      defaultWidth: 60,
      render: (_, row) => (
        <Button variant="ghost" size="sm" className="text-xs text-blue-500 hover:text-blue-700" onClick={(event) => { event.stopPropagation(); setSelected(row); }}>
          详情
        </Button>
      ),
    },
  ], []);

  const visibleColumns = useMemo(() => {
    const columnMap = new Map(columns.map((column) => [column.key, column]));
    const orderedVisible = orderedColKeys
      .map((key) => columnMap.get(key))
      .filter((column): column is Column<DisplaySearchResult> => Boolean(column && visibleCols.has(column.key)));
    const actionColumn = columnMap.get('_actions');

    return actionColumn ? [...orderedVisible, actionColumn] : orderedVisible;
  }, [columns, orderedColKeys, visibleCols]);

  const toggleDirectoryKey = (directoryKey: string) => {
    setOpenDirectoryKeys((current) => {
      const next = new Set(current);
      if (next.has(directoryKey)) {
        next.delete(directoryKey);
      } else {
        next.add(directoryKey);
      }
      return next;
    });
  };

  const handleKeywordSelect = (directoryKey: string) => {
    setSelectedKeywordDirectoryKey(directoryKey);
    setSelectedKeys(new Set());
    setSelected(null);
  };

  const renderGroupBody = (group: StageGroup) => {
    if (group.results.length === 0) {
      return (
        <Card>
          <CardContent className="p-4 text-sm text-muted-foreground">
            {group.has_placeholder ? getPlaceholderMessage(group.placeholder_reason) : '该阶段去重后没有新增商家。'}
          </CardContent>
        </Card>
      );
    }

    if (view === 'list') {
      return (
        <DataTable
          columns={visibleColumns}
          data={group.results}
          loading={false}
          rowKey={(row) => row.id}
          onRowClick={(row) => setSelected(row)}
          selectable
          selectedKeys={pickGroupSelectedKeys(selectedKeys, group.results)}
          onSelectionChange={(keys) => setSelectedKeys((current) => mergeGroupSelectedKeys(current, group.results, keys))}
          showRowIndex
          fillAvailableWidth
        />
      );
    }

    return (
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
        {group.results.map((result) => (
          <Card key={result.id} className="card-hover cursor-pointer group" onClick={() => setSelected(result)}>
            <CardContent className="p-4">
              {result.is_placeholder ? (
                <div className="flex h-32 items-center justify-center rounded-md bg-red-50 text-center text-sm font-medium text-red-600">
                  {getPlaceholderMessage(result.placeholder_reason)}
                </div>
              ) : (
                <img src={result.screenshot_public_url || result.image_url} alt={result.title} className="h-32 w-full rounded-md object-cover" />
              )}
              <h3 className="mt-3 line-clamp-2 text-sm font-medium text-foreground">{result.title}</h3>
              <p className="mt-1 text-xs text-muted-foreground">{result.merchant_name || '阶段占位结果'}</p>
              <p className="text-xs text-muted-foreground">手机 · {result.mobile || '-'}</p>
              {!result.is_placeholder && (
                <div className="mt-2 flex flex-wrap gap-2 text-xs">
                  <span className="rounded-full bg-muted px-2 py-1 text-foreground">分数 {formatScore(result.score)}</span>
                  <span className={`rounded-full px-2 py-1 font-medium ${getMatchStatusMeta(result.is_match).className}`}>{getMatchStatusMeta(result.is_match).label}</span>
                </div>
              )}
              {!result.is_placeholder && (
                <p className="mt-2 line-clamp-3 text-xs text-muted-foreground">{getMatchReasonText(result)}</p>
              )}
              <div className="mt-2 flex gap-2 opacity-0 transition-opacity group-hover:opacity-100">
                <Button size="sm" variant="outline" className="h-7 text-xs" onClick={(event) => { event.stopPropagation(); setSelected(result); }}>详情</Button>
                {result.offer_url && !result.is_placeholder && (
                  <Button size="sm" variant="outline" className="h-7 text-xs" asChild>
                    <a href={result.offer_url} target="_blank" rel="noopener noreferrer" onClick={(event) => event.stopPropagation()}>
                      <ExternalLink className="mr-1 h-3 w-3" />商品链接
                    </a>
                  </Button>
                )}
                {result.contact_url && !result.is_placeholder && (
                  <Button size="sm" variant="outline" className="h-7 text-xs" asChild>
                    <a href={result.contact_url} target="_blank" rel="noopener noreferrer" onClick={(event) => event.stopPropagation()}>
                      <MessageCircle className="mr-1 h-3 w-3" />旺旺
                    </a>
                  </Button>
                )}
              </div>
            </CardContent>
          </Card>
        ))}
      </div>
    );
  };

  const relatedLogs = selected ? allContactLogs.filter((log) => log.search_result_id === selected.id) : [];

  return (
    <div className="h-full flex flex-col gap-4">
      <PageHeader title="智能搜索" />

      <Tabs value={activeTab} className="flex-1 min-h-0 flex flex-col">
        <TabsContent value="results" className="mt-0 flex-1 min-h-0">
          <div className="flex h-full min-h-0 gap-3">
            <aside className="flex h-full w-[292px] min-w-[260px] max-w-[320px] flex-col rounded-xl border border-border bg-background">
              <div className="flex items-start justify-between gap-2 border-b border-border px-3 py-3">
                <div>
                  <div className="text-sm font-semibold text-foreground">结果目录</div>
                  <div className="text-xs text-muted-foreground">按批次 / 任务ID / 标准化搜索词逐级浏览</div>
                </div>
                <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={handleRefresh} title="刷新">
                  <RefreshCw className="h-4 w-4" />
                </Button>
              </div>
              <ScrollArea className="min-h-0 flex-1">
                <SearchResultsDirectoryTree
                  loading={loading}
                  sections={batchSections}
                  openKeys={openDirectoryKeys}
                  selectedKeywordKey={selectedKeywordDirectoryKey}
                  onToggleDirectory={toggleDirectoryKey}
                  onSelectKeyword={handleKeywordSelect}
                />
              </ScrollArea>
            </aside>

            <div className="flex min-w-0 flex-1 flex-col">
              {selectedKeywordSection ? (
                <>
                  <div className="mb-3 flex items-center gap-2 shrink-0">
                    <div className="flex rounded-md border border-border">
                      <Button variant={view === 'list' ? 'default' : 'ghost'} size="sm" onClick={() => setView('list')} className="h-9 rounded-r-none"><List className="mr-1 h-4 w-4" />列表</Button>
                      <Button variant={view === 'card' ? 'default' : 'ghost'} size="sm" onClick={() => setView('card')} className="h-9 rounded-l-none"><LayoutGrid className="mr-1 h-4 w-4" />卡片</Button>
                    </div>
                    <div className="ml-auto flex items-center gap-1">
                      <Button variant="outline" size="sm" className="h-9" onClick={() => setExportOpen(true)} disabled={visibleResults.length === 0}>
                        <Download className="mr-1 h-3.5 w-3.5" />导出
                      </Button>
                      <Button variant="destructive" size="sm" className="h-9" onClick={handleBatchDelete} disabled={selectedKeys.size === 0 || deleting || deletingGroupKey !== null}>
                        {deleting ? <LoaderCircle className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Trash2 className="mr-1 h-3.5 w-3.5" />}
                        删除选中({selectedKeys.size})
                      </Button>
                      <Button variant="outline" size="sm" className="h-9" onClick={() => setConfigOpen(true)}>
                        <SlidersHorizontal className="mr-1 h-3.5 w-3.5" />配置
                      </Button>
                    </div>
                  </div>

                  <div className="mb-3 shrink-0 border-t border-gray-300" />

                  {loading ? (
                    <div className="space-y-3">
                      {Array.from({ length: 5 }).map((_, index) => <Skeleton key={index} className="h-10 w-full" />)}
                    </div>
                  ) : (
                    <div className="flex-1 min-h-0 overflow-auto space-y-4 pr-1">
                      {visibleStageGroups.length === 0 ? (
                        <div className="flex h-full min-h-[12rem] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
                          当前标准化搜索词下暂无搜索结果
                        </div>
                      ) : visibleStageGroups.map((group) => (
                        <section key={group.groupKey} className="space-y-2">
                          {renderGroupHeader(group, {
                            showDeleteAction: canDeleteStageGroup(group),
                            deleting: deletingGroupKey === group.groupKey,
                            disableDeleteAction: deleting || (deletingGroupKey !== null && deletingGroupKey !== group.groupKey),
                            onDelete: () => void handleGroupDelete(group),
                          })}
                          {renderGroupBody(group)}
                        </section>
                      ))}
                    </div>
                  )}
                </>
              ) : (
                <div className="flex h-full min-h-[20rem] flex-col items-center justify-center rounded-xl border border-dashed bg-muted/10 px-6 text-center">
                  <Search className="mb-3 h-8 w-8 text-muted-foreground" />
                  <div className="text-base font-medium text-foreground">请选择左侧目录中的标准化搜索词</div>
                  <p className="mt-2 max-w-xl text-sm text-muted-foreground">
                    展开批次和任务节点后，点击最后一层标准化搜索词目录，右侧才会展示该类商品对应的详细搜索结果列表或卡片。
                  </p>
                </div>
              )}
            </div>
          </div>
        </TabsContent>

        <TabsContent value="tasks" className="mt-0 flex-1 min-h-0">
          <SearchTaskStatusPanel
            active={activeTab === 'tasks'}
            batchOptions={batchOptions}
            initialBatchId={taskBatchId}
            onBatchIdChange={handleTaskBatchIdChange}
          />
        </TabsContent>
      </Tabs>

      <Dialog open={!!selected} onOpenChange={(open) => !open && setSelected(null)}>
        <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
          <DialogHeader>
            <DialogTitle className="text-primary text-sm">{selected?.title}</DialogTitle>
          </DialogHeader>
          {selected && (
            <div className="space-y-4 text-sm">
              {!selected.is_placeholder && (
                <img src={selected.screenshot_public_url || selected.image_url} alt="" className="w-full max-h-48 object-contain rounded-md bg-muted" />
              )}
              {selected.is_placeholder && (
                <div className="rounded-md bg-red-50 p-4 text-sm font-medium text-red-600">
                  {getPlaceholderMessage(selected.placeholder_reason)}
                </div>
              )}
              <div className="grid grid-cols-2 gap-3">
                {([
                  ['任务ID', selected.task_id || LEGACY_TASK_LABEL],
                  ['材料名称', selected.material_name],
                  ['标准化名称', selected.standardized_name],
                  ['标准化搜索词', selected.standardized_search_term],
                  ['阶段', selected.stage_label || '-'],
                  ['标题分数', formatScore(selected.score)],
                  ['LLM 判定', getMatchStatusMeta(selected.is_match).label],
                  ['商家名称', selected.merchant_name],
                  ['联系人', selected.contact_person],
                  ['电话', selected.phone],
                  ['手机', selected.mobile],
                  ['地址', selected.address],
                  ['旺旺ID', selected.contact_handle],
                  ['搜索关键词', selected.keyword],
                ] as [string, string][]).map(([label, value]) => (
                  <div key={label}>
                    <span className="text-muted-foreground">{label}</span>
                    <p className="text-foreground">{value || '-'}</p>
                  </div>
                ))}
              </div>
              {!selected.is_placeholder && (
                <div className="rounded-md border border-border bg-muted/20 p-3">
                  <div className="text-xs text-muted-foreground">判定原因</div>
                  <p className="mt-1 text-sm text-foreground">{getMatchReasonText(selected)}</p>
                </div>
              )}
              <div className="flex gap-2">
                {selected.offer_url && !selected.is_placeholder && (
                  <Button size="sm" variant="outline" asChild>
                    <a href={selected.offer_url} target="_blank" rel="noopener noreferrer"><ExternalLink className="h-3 w-3 mr-1" />商品链接</a>
                  </Button>
                )}
                {selected.merchant_url && !selected.is_placeholder && (
                  <Button size="sm" variant="outline" asChild>
                    <a href={selected.merchant_url} target="_blank" rel="noopener noreferrer"><ExternalLink className="h-3 w-3 mr-1" />店铺链接</a>
                  </Button>
                )}
                {selected.contact_url && !selected.is_placeholder && (
                  <Button size="sm" variant="outline" asChild>
                    <a href={selected.contact_url} target="_blank" rel="noopener noreferrer"><MessageCircle className="h-3 w-3 mr-1" />旺旺</a>
                  </Button>
                )}
              </div>
              {relatedLogs.length > 0 && (
                <div>
                  <h4 className="font-medium text-primary mb-2">关联联系记录</h4>
                  {relatedLogs.map((log) => (
                    <div key={log.id} className="p-2 bg-muted rounded-md text-xs mb-1">
                      <span className="text-muted-foreground">{formatDate(log.created_at)}</span> · 状态: {log.status} · {log.message_sent || '无消息'}
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}
        </DialogContent>
      </Dialog>

      <ColumnConfigDialog
        open={configOpen}
        onOpenChange={setConfigOpen}
        columns={SEARCH_RESULT_COLUMN_OPTIONS.map((column) => ({ ...column, required: false }))}
        visibleCols={visibleCols}
        orderedKeys={orderedColKeys}
        onConfirm={({ visibleCols: nextVisibleCols, orderedKeys: nextOrderedKeys }) => {
          setColumnConfig({
            visibleCols: nextVisibleCols,
            orderedKeys: normalizeSearchResultColumnOrder(nextOrderedKeys),
          });
        }}
      />

      <ExportDialog
        open={exportOpen}
        onOpenChange={setExportOpen}
        data={exportData}
        totalCount={visibleResults.length}
        filename={`搜索结果_${new Date().toLocaleDateString('zh-CN').replace(/\//g, '-')}.xlsx`}
        columns={[
          { key: 'batch_id', label: '批次ID', group: '基础字段' },
          { key: 'task_id', label: '任务ID', group: '基础字段' },
          { key: 'stage_label', label: '阶段', group: '基础字段' },
          { key: 'material_name', label: '材料名称', group: '基础字段' },
          { key: 'standardized_name', label: '标准化名称', group: '基础字段' },
          { key: 'standardized_search_term', label: '标准化搜索词', group: '基础字段' },
          { key: 'keyword', label: '搜索关键词', group: '基础字段' },
          { key: 'score', label: '标题分数', group: '基础字段' },
          { key: 'is_match', label: 'LLM判定', group: '基础字段' },
          { key: 'reason', label: '判定原因', group: '基础字段' },
          { key: 'title', label: '商品标题', group: '基础字段' },
          { key: 'merchant_name', label: '商家名称', group: '基础字段' },
          { key: 'phone', label: '电话', group: '基础字段' },
          { key: 'mobile', label: '手机', group: '基础字段' },
          { key: 'created_at', label: '时间', group: '基础字段' },
          { key: 'offer_url', label: '商品链接', group: '扩展字段', defaultChecked: false },
          { key: 'merchant_url', label: '店铺链接', group: '扩展字段', defaultChecked: false },
          { key: 'contact_type', label: '联系方式类型', group: '扩展字段', defaultChecked: false },
          { key: 'contact_url', label: '联系链接', group: '扩展字段', defaultChecked: false },
          { key: 'contact_handle', label: '联系账号', group: '扩展字段', defaultChecked: false },
          { key: 'contact_person', label: '联系人', group: '扩展字段', defaultChecked: false },
          { key: 'address', label: '地址', group: '扩展字段', defaultChecked: false },
          { key: 'placeholder_reason', label: '占位原因', group: '扩展字段', defaultChecked: false },
        ]}
      />
    </div>
  );
}
