import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { Download, ExternalLink, LayoutGrid, List, LoaderCircle, MessageCircle, PanelLeftClose, PanelLeftOpen, RefreshCw, RotateCcw, SlidersHorizontal, Trash2, ZoomIn, ZoomOut } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { useProcurementBatchOptions } from '@/hooks/useProcurementBatchOptions';
import { useSearchResultsBundleQuery } from '@/hooks/useRealtimeDataQueries';
import { formatDate } from '@/lib/dateUtils';
import { getStoredColumnConfig, resolveStoredColumnConfig, saveStoredColumnConfig } from '@/lib/columnConfigStorage';
import { translateFailureReason } from '@/lib/failureReasonMessages';
import { deleteSearchResultsByIds, deleteSearchStageGroup } from '@/services/searchService';
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;
  match_bucket_label: 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 SearchResultsBundle = {
  searchResults: SearchResult[];
  stageSummaries: SearchStageSummary[];
  merchantContacts: MerchantContact[];
  materials: ProcurementMaterial[];
  standardized: StandardizedMaterial[];
  contactLogs: ContactLog[];
};

type SearchResultsTabValue = 'results' | 'tasks';
type MatchBucketFilter = 'all' | 'matched' | 'possible' | 'not_match';

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: 'match_bucket', 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_DIRECTORY_HIDDEN_KEY = 'autoprocurement.search-results.directoryHidden';
const SEARCH_RESULT_COLUMN_KEYS = SEARCH_RESULT_COLUMN_OPTIONS.map((column) => column.key);
const UPGRADED_DEFAULT_VISIBLE_KEYS = ['score', 'match_bucket', 'is_match', 'reason'] as const;
const ALL_FILTER_VALUE = '__all__';
const LEGACY_TASK_ID = '__legacy__';
const LEGACY_TASK_LABEL = '历史结果';
const SCREENSHOT_PREVIEW_MIN_ZOOM = 0.5;
const SCREENSHOT_PREVIEW_MAX_ZOOM = 3;
const SCREENSHOT_PREVIEW_ZOOM_STEP = 0.25;

const MATCH_BUCKET_FILTER_OPTIONS: Array<{ value: MatchBucketFilter; label: string }> = [
  { value: 'all', label: '全部商家' },
  { value: 'matched', label: '匹配商家' },
  { value: 'possible', label: '可能商家' },
  { value: 'not_match', 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 getProductFilterLabel(group: Pick<StageGroup, 'standardized_search_term' | 'standardized_name' | 'material_name'>) {
  return group.standardized_search_term || group.standardized_name || group.material_name || '-';
}

function readDirectoryHiddenPreference() {
  if (typeof window === 'undefined') {
    return false;
  }

  return typeof window.localStorage?.getItem === 'function'
    ? window.localStorage.getItem(SEARCH_RESULT_DIRECTORY_HIDDEN_KEY) === '1'
    : false;
}

const filterSelectTriggerClassName = 'h-8 w-full rounded-md border border-[hsl(220_13%_88%)] bg-[hsl(220_14%_97%)] px-2 text-[11px] font-semibold text-foreground shadow-sm focus:ring-2 focus:ring-inset focus:ring-ring focus:ring-offset-0 [&>span]:truncate';
const filterSelectContentClassName = 'min-w-[var(--radix-select-trigger-width)] rounded-lg border border-[hsl(220_13%_88%)] bg-white p-1 shadow-lg';
const filterSelectItemClassName = 'h-8 rounded-md text-xs font-medium data-[state=checked]:bg-[hsl(221_83%_95%)] data-[state=checked]:text-[hsl(221_83%_40%)]';

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 getMatchBucketMeta(value: string | null | undefined) {
  if (value === 'possible') {
    return {
      label: '可能商家',
      className: 'bg-amber-50 text-amber-700',
    };
  }

  if (value === 'matched') {
    return {
      label: '匹配商家',
      className: 'bg-green-50 text-green-700',
    };
  }

  if (value === 'not_match') {
    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' | 'match_bucket'>) {
  if (result.is_placeholder) {
    return '';
  }

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

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

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

function getSearchResultImageUrl(result: Pick<SearchResult, 'screenshot_public_url' | 'image_url'>) {
  return (result.screenshot_public_url || result.image_url || '').trim();
}

function clampScreenshotPreviewZoom(value: number) {
  return Math.min(SCREENSHOT_PREVIEW_MAX_ZOOM, Math.max(SCREENSHOT_PREVIEW_MIN_ZOOM, value));
}

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'>('card');
  const [deleting, setDeleting] = useState(false);
  const [deletingGroupKey, setDeletingGroupKey] = useState<string | null>(null);
  const [selected, setSelected] = useState<DisplaySearchResult | null>(null);
  const [previewImage, setPreviewImage] = useState<{ src: string; alt: string } | null>(null);
  const [previewZoom, setPreviewZoom] = useState(1);
  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 [directoryHidden, setDirectoryHidden] = useState(() => readDirectoryHiddenPreference());
  const [filterBatchId, setFilterBatchId] = useState(() => searchParams.get('batch_id') ?? '');
  const [filterTaskId, setFilterTaskId] = useState(() => (searchParams.get('batch_id') ? searchParams.get('task_id') ?? '' : ''));
  const [filterProduct, setFilterProduct] = useState(() => searchParams.get('product') ?? '');
  const [matchBucketFilter, setMatchBucketFilter] = useState<MatchBucketFilter>('all');
  const [filtersInitialized, setFiltersInitialized] = useState(false);
  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 searchResultsBundleQuery = useSearchResultsBundleQuery();
  const {
    data: searchResultsBundle,
    error: searchResultsBundleError,
    isLoading: searchResultsBundleLoading,
    refetch: refetchSearchResultsBundle,
  } = searchResultsBundleQuery;
  const activeTabParam = searchParams.get('tab');
  const activeTab: SearchResultsTabValue = activeTabParam === 'tasks' ? 'tasks' : 'results';
  const taskBatchId = searchParams.get('batch_id') ?? '';

  const applySearchResultsBundle = useCallback((bundle: SearchResultsBundle) => {
    setAllSearchResults(bundle.searchResults);
    setAllStageSummaries(bundle.stageSummaries);
    setAllMerchantContacts(bundle.merchantContacts);
    setAllMaterials(bundle.materials);
    setAllStandardized(bundle.standardized);
    setAllContactLogs(bundle.contactLogs);
  }, []);

  const loadData = useCallback(async () => {
    const { data } = await refetchSearchResultsBundle();
    if (data) {
      applySearchResultsBundle(data);
    }
  }, [applySearchResultsBundle, refetchSearchResultsBundle]);

  useEffect(() => {
    if (searchResultsBundle) {
      applySearchResultsBundle(searchResultsBundle);
    }
  }, [applySearchResultsBundle, searchResultsBundle]);

  useEffect(() => {
    setLoading(searchResultsBundleLoading);
    if (searchResultsBundleError) {
      console.error('SearchResults data load error:', searchResultsBundleError);
    }
  }, [searchResultsBundleError, searchResultsBundleLoading]);

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

  useEffect(() => {
    if (typeof window !== 'undefined' && typeof window.localStorage?.setItem === 'function') {
      window.localStorage.setItem(SEARCH_RESULT_DIRECTORY_HIDDEN_KEY, directoryHidden ? '1' : '0');
    }
  }, [directoryHidden]);

  useEffect(() => {
    if (searchParams.get('task_id') && !searchParams.get('batch_id')) {
      toast.info('请先选择批次 ID 后再筛选任务 ID');
      const nextParams = new URLSearchParams(searchParams);
      nextParams.delete('task_id');
      setSearchParams(nextParams, { replace: true });
    }
  }, [searchParams, setSearchParams]);

  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
      || '-',
    match_bucket_label: getMatchBucketMeta(result.match_bucket).label,
    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 batchFilterOptions = useMemo(() => {
    const labels = new Map(batchOptions.map((option) => [option.value, option.label]));
    const ids = [...new Set(stageGroups.map((group) => group.batch_id).filter(Boolean))];

    return ids
      .sort((left, right) => right.localeCompare(left))
      .map((batchId) => ({
        value: batchId,
        label: labels.get(batchId) ?? batchId,
      }));
  }, [batchOptions, stageGroups]);

  const taskFilterOptions = useMemo(() => {
    if (!filterBatchId) {
      return [];
    }

    const tasks = new Map<string, { value: string; label: string; latest_created_at: string }>();
    stageGroups
      .filter((group) => group.batch_id === filterBatchId)
      .forEach((group) => {
        const value = group.task_id || LEGACY_TASK_ID;
        const existing = tasks.get(value);
        if (!existing || group.created_at > existing.latest_created_at) {
          tasks.set(value, {
            value,
            label: getTaskLabel(group.task_id),
            latest_created_at: group.created_at,
          });
        }
      });

    return [...tasks.values()]
      .sort((left, right) => {
        if (left.value === LEGACY_TASK_ID) {
          return 1;
        }
        if (right.value === LEGACY_TASK_ID) {
          return -1;
        }
        return right.latest_created_at.localeCompare(left.latest_created_at) || left.label.localeCompare(right.label);
      });
  }, [filterBatchId, stageGroups]);

  const productFilterOptions = useMemo(() => {
    const normalizedTaskFilter = filterBatchId ? filterTaskId : '';
    const products = new Map<string, { value: string; label: string; latest_created_at: string }>();

    stageGroups
      .filter((group) => {
        if (filterBatchId && group.batch_id !== filterBatchId) {
          return false;
        }
        if (normalizedTaskFilter && (group.task_id || LEGACY_TASK_ID) !== normalizedTaskFilter) {
          return false;
        }
        return true;
      })
      .forEach((group) => {
        const label = getProductFilterLabel(group);
        const existing = products.get(label);
        if (!existing || group.created_at > existing.latest_created_at) {
          products.set(label, {
            value: label,
            label,
            latest_created_at: group.created_at,
          });
        }
      });

    if (filterProduct && !products.has(filterProduct)) {
      products.set(filterProduct, {
        value: filterProduct,
        label: filterProduct,
        latest_created_at: '',
      });
    }

    return [...products.values()]
      .sort((left, right) => right.latest_created_at.localeCompare(left.latest_created_at) || left.label.localeCompare(right.label));
  }, [filterBatchId, filterProduct, filterTaskId, stageGroups]);

  const visibleStageGroups = useMemo<StageGroup[]>(() => {
    const normalizedTaskFilter = filterBatchId ? filterTaskId : '';

    return stageGroups.filter((group) => {
      if (filterBatchId && group.batch_id !== filterBatchId) {
        return false;
      }
      if (normalizedTaskFilter && (group.task_id || LEGACY_TASK_ID) !== normalizedTaskFilter) {
        return false;
      }
      if (filterProduct && getProductFilterLabel(group) !== filterProduct) {
        return false;
      }
      if (matchBucketFilter !== 'all') {
        return group.results.some((result) => !result.is_placeholder && result.match_bucket === matchBucketFilter);
      }
      return true;
    }).map((group) => {
      if (matchBucketFilter === 'all') {
        return group;
      }

      const results = group.results.filter((result) => !result.is_placeholder && result.match_bucket === matchBucketFilter);
      return {
        ...group,
        results,
        merchant_count: results.length,
        has_placeholder: false,
        placeholder_reason: '',
      };
    });
  }, [filterBatchId, filterProduct, filterTaskId, matchBucketFilter, stageGroups]);

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

  const matchingKeywordDirectoryKey = useMemo(() => {
    if (!filterProduct) {
      return null;
    }

    for (const batchSection of batchSections) {
      if (filterBatchId && batchSection.batch_id !== filterBatchId) {
        continue;
      }

      for (const taskSection of batchSection.taskSections) {
        if (filterBatchId && filterTaskId && (taskSection.task_id || LEGACY_TASK_ID) !== filterTaskId) {
          continue;
        }

        const matched = taskSection.keywordSections.find((keywordSection) => keywordSection.keyword === filterProduct);
        if (matched) {
          return matched.directoryKey;
        }
      }
    }

    return null;
  }, [batchSections, filterBatchId, filterProduct, filterTaskId]);

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

  useEffect(() => {
    if (loading || filtersInitialized) {
      return;
    }

    const hasUrlFilters = Boolean(searchParams.get('batch_id') || searchParams.get('task_id') || searchParams.get('product'));
    if (hasUrlFilters) {
      setFiltersInitialized(true);
      return;
    }

    const latestTaskResult = [...displayResults]
      .filter((result) => Boolean(normalizeTaskId(result.task_id)))
      .sort((left, right) => right.created_at.localeCompare(left.created_at))[0];

    if (latestTaskResult) {
      setFilterBatchId(latestTaskResult.batch_id);
      setFilterTaskId(normalizeTaskId(latestTaskResult.task_id) ?? '');
    }

    setFiltersInitialized(true);
  }, [displayResults, filtersInitialized, loading, searchParams]);

  useEffect(() => {
    if (!filterBatchId && filterTaskId) {
      setFilterTaskId('');
      return;
    }

    if (!loading && filterBatchId && filterTaskId && !taskFilterOptions.some((option) => option.value === filterTaskId)) {
      setFilterTaskId('');
    }
  }, [filterBatchId, filterTaskId, loading, taskFilterOptions]);

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

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

  useEffect(() => {
    if (!filtersInitialized || activeTab !== 'results') {
      return;
    }

    const nextParams = new URLSearchParams(searchParams);
    if (filterBatchId) {
      nextParams.set('batch_id', filterBatchId);
    } else {
      nextParams.delete('batch_id');
    }

    if (filterBatchId && filterTaskId) {
      nextParams.set('task_id', filterTaskId);
    } else {
      nextParams.delete('task_id');
    }

    if (filterProduct) {
      nextParams.set('product', filterProduct);
    } else {
      nextParams.delete('product');
    }

    if (nextParams.toString() !== searchParams.toString()) {
      setSearchParams(nextParams, { replace: true });
    }
  }, [activeTab, filterBatchId, filterProduct, filterTaskId, filtersInitialized, searchParams, setSearchParams]);

  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 handleBatchFilterChange = (nextBatchId: string) => {
    setFilterBatchId(nextBatchId);
    if (!nextBatchId) {
      setFilterTaskId('');
    }
    setSelectedKeys(new Set());
    setSelected(null);
  };

  const handleTaskFilterChange = (nextTaskId: string) => {
    if (!filterBatchId && nextTaskId) {
      toast.info('请先选择批次 ID 后再筛选任务 ID');
      return;
    }

    setFilterTaskId(nextTaskId);
    setSelectedKeys(new Set());
    setSelected(null);
  };

  const handleProductFilterChange = (nextProduct: string) => {
    setFilterProduct(nextProduct);
    setSelectedKeys(new Set());
    setSelected(null);
  };

  const clearResultFilters = () => {
    setFilterBatchId('');
    setFilterTaskId('');
    setFilterProduct('');
    setMatchBucketFilter('all');
    setSelectedKeywordDirectoryKey(null);
    setSelectedKeys(new Set());
    setSelected(null);
  };

  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({
        taskId: group.task_id,
        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: 'match_bucket',
      label: '商家分流',
      defaultWidth: 110,
      render: (value, row) => {
        if (row.is_placeholder) {
          return <span className="text-muted-foreground">-</span>;
        }

        const meta = getMatchBucketMeta(value as string | 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) => {
    const matchedBatch = batchSections.find((batchSection) => batchSection.directoryKey === directoryKey);
    if (matchedBatch) {
      handleBatchFilterChange(matchedBatch.batch_id);
    } else {
      const matchedTask = batchSections
        .flatMap((batchSection) => batchSection.taskSections)
        .find((taskSection) => taskSection.directoryKey === directoryKey);

      if (matchedTask) {
        setFilterBatchId(matchedTask.batch_id);
        setFilterTaskId(matchedTask.task_id || LEGACY_TASK_ID);
        setSelectedKeys(new Set());
        setSelected(null);
      }
    }

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

  const handleKeywordSelect = (directoryKey: string) => {
    const keywordSection = keywordSectionLookup.get(directoryKey);
    if (keywordSection) {
      setFilterBatchId(keywordSection.batch_id);
      setFilterTaskId(keywordSection.task_id || LEGACY_TASK_ID);
      setFilterProduct(keywordSection.keyword);
    }

    setSelectedKeywordDirectoryKey(directoryKey);
    setSelectedKeys(new Set());
    setSelected(null);
  };

  const openScreenshotPreview = (result: DisplaySearchResult) => {
    if (result.is_placeholder) {
      return;
    }

    const src = getSearchResultImageUrl(result);
    if (!src) {
      return;
    }

    setPreviewZoom(1);
    setPreviewImage({ src, alt: result.title || '商品截图' });
  };

  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={getSearchResultImageUrl(result)} 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 ${getMatchBucketMeta(result.match_bucket).className}`}>{getMatchBucketMeta(result.match_bucket).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">
                <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-0">
            <div
              className={`h-full shrink-0 overflow-hidden transition-all duration-300 ease-in-out ${
                directoryHidden ? 'mr-0 w-0 opacity-0' : 'mr-3 w-[292px] opacity-100'
              }`}
              aria-hidden={directoryHidden}
            >
              <aside
                className={`flex h-full w-[292px] flex-col rounded-xl border border-border bg-background transition-transform duration-300 ease-in-out ${
                  directoryHidden ? '-translate-x-3' : 'translate-x-0'
                }`}
              >
                <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>
                  <div className="flex shrink-0 gap-1">
                    <Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleRefresh} title="刷新">
                      <RefreshCw className="h-4 w-4" />
                    </Button>
                    <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setDirectoryHidden(true)} title="隐藏目录">
                      <PanelLeftClose className="h-4 w-4" />
                    </Button>
                  </div>
                </div>
                <ScrollArea className="min-h-0 flex-1">
                  <SearchResultsDirectoryTree
                    loading={loading}
                    sections={batchSections}
                    openKeys={openDirectoryKeys}
                    selectedKeywordKey={selectedKeywordDirectoryKey}
                    onToggleDirectory={toggleDirectoryKey}
                    onSelectKeyword={handleKeywordSelect}
                  />
                </ScrollArea>
              </aside>
            </div>

            <div className="flex min-w-0 flex-1 flex-col">
              <div className="mb-3 shrink-0 rounded-lg border border-border/80 bg-muted/20 px-3 py-2">
                <div className="flex flex-nowrap items-end gap-1.5 overflow-x-auto py-0.5">
                  {directoryHidden && (
                    <Button variant="outline" size="sm" className="h-8 w-[96px] shrink-0 justify-start overflow-hidden px-2 text-xs" onClick={() => setDirectoryHidden(false)}>
                      <PanelLeftOpen className="mr-1 h-3.5 w-3.5" />显示目录
                    </Button>
                  )}
                  <label className="flex w-[272px] shrink-0 items-center gap-2 text-xs font-medium text-muted-foreground">
                    <span className="shrink-0 whitespace-nowrap">批次</span>
                    <Select
                      value={filterBatchId || ALL_FILTER_VALUE}
                      onValueChange={(value) => handleBatchFilterChange(value === ALL_FILTER_VALUE ? '' : value)}
                    >
                      <SelectTrigger className={filterSelectTriggerClassName}>
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent className={filterSelectContentClassName}>
                        <SelectItem value={ALL_FILTER_VALUE} className={filterSelectItemClassName}>全部批次</SelectItem>
                        {batchFilterOptions.map((option) => (
                          <SelectItem key={option.value} value={option.value} className={filterSelectItemClassName}>{option.label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </label>
                  <label className="flex w-[292px] shrink-0 items-center gap-2 text-xs font-medium text-muted-foreground">
                    <span className="shrink-0 whitespace-nowrap">任务 ID</span>
                    <Select
                      value={filterBatchId && filterTaskId ? filterTaskId : ALL_FILTER_VALUE}
                      onValueChange={(value) => handleTaskFilterChange(value === ALL_FILTER_VALUE ? '' : value)}
                      disabled={!filterBatchId}
                    >
                      <SelectTrigger
                        title={filterBatchId ? '筛选任务 ID' : '请先选择批次 ID'}
                        className={filterSelectTriggerClassName}
                      >
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent className={filterSelectContentClassName}>
                        <SelectItem value={ALL_FILTER_VALUE} className={filterSelectItemClassName}>
                          {filterBatchId ? '全部任务' : '请先选择批次 ID'}
                        </SelectItem>
                        {taskFilterOptions.map((option) => (
                          <SelectItem key={option.value} value={option.value} className={filterSelectItemClassName}>{option.label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </label>
                  <label className="flex w-[136px] shrink-0 items-center gap-2 text-xs font-medium text-muted-foreground">
                    <span className="shrink-0 whitespace-nowrap">商品</span>
                    <Select
                      value={filterProduct || ALL_FILTER_VALUE}
                      onValueChange={(value) => handleProductFilterChange(value === ALL_FILTER_VALUE ? '' : value)}
                    >
                      <SelectTrigger className={filterSelectTriggerClassName}>
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent className={filterSelectContentClassName}>
                        <SelectItem value={ALL_FILTER_VALUE} className={filterSelectItemClassName}>全部商品</SelectItem>
                        {productFilterOptions.map((option) => (
                          <SelectItem key={option.value} value={option.value} className={filterSelectItemClassName}>{option.label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </label>
                  <label className="flex w-[156px] shrink-0 items-center gap-2 text-xs font-medium text-muted-foreground">
                    <span className="shrink-0 whitespace-nowrap">商家分流</span>
                    <Select
                      value={matchBucketFilter}
                      onValueChange={(value) => setMatchBucketFilter(value as MatchBucketFilter)}
                    >
                      <SelectTrigger className={filterSelectTriggerClassName}>
                        <SelectValue />
                      </SelectTrigger>
                      <SelectContent className={filterSelectContentClassName}>
                        {MATCH_BUCKET_FILTER_OPTIONS.map((option) => (
                          <SelectItem key={option.value} value={option.value} className={filterSelectItemClassName}>{option.label}</SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </label>
                  <Button variant="ghost" size="sm" className="h-8 shrink-0 px-2 text-xs" onClick={clearResultFilters}>
                    <RotateCcw className="mr-1 h-3.5 w-3.5" />重置
                  </Button>
                  <Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={handleRefresh} title="刷新">
                    <RefreshCw className="h-3.5 w-3.5" />
                  </Button>
                  <div className="ml-auto shrink-0 whitespace-nowrap pb-1.5 text-[11px] font-medium text-muted-foreground">
                    {visibleStageGroups.length} 组 / {visibleResults.length} 条结果
                  </div>
                </div>
              </div>

              <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>
          </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) => {
        if (!open) {
          setSelected(null);
          setPreviewImage(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 && getSearchResultImageUrl(selected) && (
                <button
                  type="button"
                  className="group relative block w-full overflow-hidden rounded-md bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
                  onClick={() => openScreenshotPreview(selected)}
                  aria-label="预览商品截图"
                >
                  <img src={getSearchResultImageUrl(selected)} alt={selected.title || '商品截图'} className="max-h-48 w-full object-contain transition-transform group-hover:scale-[1.01]" />
                  <span className="absolute bottom-2 right-2 rounded-md bg-background/90 px-2 py-1 text-xs text-foreground shadow-sm">
                    点击预览
                  </span>
                </button>
              )}
              {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)],
                  ['商家分流', getMatchBucketMeta(selected.match_bucket).label],
                  ['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>

      <Dialog open={!!previewImage} onOpenChange={(open) => !open && setPreviewImage(null)}>
        <DialogContent className="flex h-[88vh] max-w-[92vw] flex-col gap-0 overflow-hidden p-0">
          <DialogHeader className="border-b border-border px-4 py-3 pr-14">
            <div className="flex items-center gap-3">
              <DialogTitle className="min-w-0 flex-1 truncate text-sm text-primary">商品截图预览</DialogTitle>
              <div className="flex shrink-0 items-center gap-1">
                <Button
                  type="button"
                  variant="outline"
                  size="icon"
                  className="h-8 w-8"
                  onClick={() => setPreviewZoom((current) => clampScreenshotPreviewZoom(current - SCREENSHOT_PREVIEW_ZOOM_STEP))}
                  disabled={previewZoom <= SCREENSHOT_PREVIEW_MIN_ZOOM}
                  title="缩小"
                  aria-label="缩小"
                >
                  <ZoomOut className="h-4 w-4" />
                </Button>
                <div className="w-14 text-center text-xs tabular-nums text-muted-foreground">{Math.round(previewZoom * 100)}%</div>
                <Button
                  type="button"
                  variant="outline"
                  size="icon"
                  className="h-8 w-8"
                  onClick={() => setPreviewZoom((current) => clampScreenshotPreviewZoom(current + SCREENSHOT_PREVIEW_ZOOM_STEP))}
                  disabled={previewZoom >= SCREENSHOT_PREVIEW_MAX_ZOOM}
                  title="放大"
                  aria-label="放大"
                >
                  <ZoomIn className="h-4 w-4" />
                </Button>
                <Button
                  type="button"
                  variant="outline"
                  size="icon"
                  className="h-8 w-8"
                  onClick={() => setPreviewZoom(1)}
                  disabled={previewZoom === 1}
                  title="重置缩放"
                  aria-label="重置缩放"
                >
                  <RotateCcw className="h-4 w-4" />
                </Button>
              </div>
            </div>
          </DialogHeader>
          <div className="min-h-0 flex-1 overflow-auto bg-muted/40 p-4">
            {previewImage && (
              <div className="flex min-h-full min-w-full items-start justify-center">
                <img
                  src={previewImage.src}
                  alt={previewImage.alt}
                  className="max-w-none origin-top rounded-md bg-background shadow-sm"
                  style={{ width: `${previewZoom * 100}%` }}
                />
              </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: 'match_bucket_label', 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>
  );
}
