import { Button, Card, Form, Select, SideSheet, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { IconCopy, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { useEffect, useMemo, useState } from 'react';
import { api, type RawFrameQuery } from '../api/client';
import type { HistoryLocationRow, Page, RawFrameRow } from '../api/types';
import { VehicleMap, type VehicleMapPoint } from '../components/VehicleMap';
import { PageHeader } from '../components/PageHeader';
import { isAMapConfigured } from '../config/appConfig';
import { buildAppHash } from '../domain/appRoute';
import { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';

type HistoryFilters = {
  keyword?: string;
  protocol?: string;
  dateFrom?: string;
  dateTo?: string;
  fields?: string;
  includeFields?: boolean | string;
};

type RawFieldRow = {
  id: string;
  rawId: string;
  vin: string;
  plate: string;
  protocol: string;
  frameType: string;
  deviceTime: string;
  serverTime: string;
  fieldPath: string;
  fieldValue: unknown;
};

const defaultFilters: HistoryFilters = {
  keyword: 'LB9A32A24R0LS1426',
  includeFields: false
};

const defaultPage = { items: [], total: 0, limit: 10, offset: 0 };

function canOpenVehicle(vin?: string) {
  const value = vin?.trim();
  return Boolean(value && value !== 'unknown');
}

function dateOnly(value?: string) {
  const match = String(value ?? '').match(/^(\d{4})-(\d{2})-(\d{2})/);
  return match ? `${match[1]}-${match[2]}-${match[3]}` : '';
}

function nextDate(value: string) {
  const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) return '';
  const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]) + 1));
  return date.toISOString().slice(0, 10);
}

function splitFields(value?: string) {
  return (value ?? '')
    .split(/[\n,]/)
    .map((item) => item.trim())
    .filter(Boolean);
}

type HistoryTabKey = 'location' | 'raw' | 'fields';

function normalizeHistoryTab(value?: string): HistoryTabKey {
  if (value === 'fields') return 'fields';
  return value === 'raw' ? 'raw' : 'location';
}

function isIncludeFieldsEnabled(value?: boolean | string) {
  return value === true || value === 'true';
}

function hasRawFieldQueryScope(filters: HistoryFilters) {
  return Boolean(
    filters.keyword?.trim() ||
    filters.dateFrom?.trim() ||
    filters.dateTo?.trim() ||
    splitFields(filters.fields).length > 0
  );
}

function shouldBlockRawFieldQuery(filters: HistoryFilters) {
  return isIncludeFieldsEnabled(filters.includeFields) && !hasRawFieldQueryScope(filters);
}

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function hasValidCoordinate(row: HistoryLocationRow) {
  return isFiniteNumber(row.longitude) && isFiniteNumber(row.latitude) && row.longitude !== 0 && row.latitude !== 0;
}

function formatNumber(value?: number, suffix = '') {
  if (!isFiniteNumber(value)) return '-';
  return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}${suffix}`;
}

function formatPercent(value?: number) {
  if (!isFiniteNumber(value)) return '-';
  return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })}%`;
}

function timeValue(row?: HistoryLocationRow) {
  const value = row?.deviceTime || row?.serverTime || row?.lastSeen;
  const ms = Date.parse(String(value ?? '').replace(' ', 'T'));
  return Number.isFinite(ms) ? ms : undefined;
}

function playbackPointId(row: HistoryLocationRow, index: number) {
  return `${row.vin || 'unknown'}-${row.deviceTime || row.serverTime || row.lastSeen || index}-${index}`;
}

function formatDurationMinutes(value?: number, suffix = '') {
  if (!isFiniteNumber(value)) return '-';
  if (value < 1) return `${Math.round(value * 60)} 秒${suffix}`;
  return `${value.toLocaleString(undefined, { maximumFractionDigits: 1 })} 分钟${suffix}`;
}

type TrajectoryAnomalySummary = {
  gapCount: number;
  maxGapMinutes?: number;
  mileageRollbackCount: number;
  maxMileageRollbackKm?: number;
  overspeedCount: number;
  maxSpeedKmh?: number;
};

function analyzeTrajectoryAnomalies(rows: HistoryLocationRow[]): TrajectoryAnomalySummary {
  const summary: TrajectoryAnomalySummary = {
    gapCount: 0,
    mileageRollbackCount: 0,
    overspeedCount: 0
  };
  let previousTime: number | undefined;
  let previousMileage: number | undefined;
  for (const row of rows) {
    const currentTime = timeValue(row);
    if (currentTime != null && previousTime != null) {
      const gapMinutes = Math.abs(currentTime - previousTime) / 60000;
      if (gapMinutes > 30) {
        summary.gapCount += 1;
        summary.maxGapMinutes = summary.maxGapMinutes == null ? gapMinutes : Math.max(summary.maxGapMinutes, gapMinutes);
      }
    }
    if (currentTime != null) {
      previousTime = currentTime;
    }

    if (isFiniteNumber(row.totalMileageKm) && previousMileage != null && row.totalMileageKm < previousMileage) {
      const rollback = previousMileage - row.totalMileageKm;
      summary.mileageRollbackCount += 1;
      summary.maxMileageRollbackKm = summary.maxMileageRollbackKm == null ? rollback : Math.max(summary.maxMileageRollbackKm, rollback);
    }
    if (isFiniteNumber(row.totalMileageKm)) {
      previousMileage = row.totalMileageKm;
    }

    if (isFiniteNumber(row.speedKmh) && row.speedKmh > 120) {
      summary.overspeedCount += 1;
      summary.maxSpeedKmh = summary.maxSpeedKmh == null ? row.speedKmh : Math.max(summary.maxSpeedKmh, row.speedKmh);
    }
  }
  return summary;
}

function mergeInitialFilters(initialVin: string, initialProtocol?: string, initialFilters: Record<string, string> = {}): HistoryFilters {
  const hasExplicitFilters = Object.keys(initialFilters).length > 0;
  return {
    ...defaultFilters,
    keyword: initialVin || (hasExplicitFilters ? '' : defaultFilters.keyword),
    protocol: initialProtocol,
    ...initialFilters,
    includeFields: isIncludeFieldsEnabled(initialFilters.includeFields)
  };
}

const locationExportColumns: CsvColumn<HistoryLocationRow>[] = [
  { title: 'VIN', value: (row) => row.vin },
  { title: '车牌', value: (row) => row.plate },
  { title: '数据来源', value: (row) => row.protocol },
  { title: '经度', value: (row) => row.longitude },
  { title: '纬度', value: (row) => row.latitude },
  { title: '速度km/h', value: (row) => row.speedKmh },
  { title: '总里程km', value: (row) => row.totalMileageKm },
  { title: '设备时间', value: (row) => row.deviceTime },
  { title: '入库时间', value: (row) => row.serverTime }
];

const rawExportColumns: CsvColumn<RawFrameRow>[] = [
  { title: 'ID', value: (row) => row.id },
  { title: 'VIN', value: (row) => row.vin },
  { title: '车牌', value: (row) => row.plate },
  { title: '数据来源', value: (row) => row.protocol },
  { title: '帧类型', value: (row) => row.frameType },
  { title: '大小B', value: (row) => row.rawSizeBytes },
  { title: '设备时间', value: (row) => row.deviceTime },
  { title: '入库时间', value: (row) => row.serverTime },
  { title: '解析字段', value: (row) => row.parsedFields ?? {} }
];

const rawFieldExportColumns: CsvColumn<RawFieldRow>[] = [
  { title: 'RAW ID', value: (row) => row.rawId },
  { title: 'VIN', value: (row) => row.vin },
  { title: '车牌', value: (row) => row.plate },
  { title: '数据来源', value: (row) => row.protocol },
  { title: '帧类型', value: (row) => row.frameType },
  { title: '字段', value: (row) => row.fieldPath },
  { title: '值', value: (row) => formatFieldValue(row.fieldValue) },
  { title: '设备时间', value: (row) => row.deviceTime },
  { title: '入库时间', value: (row) => row.serverTime }
];

function formatFieldValue(value: unknown) {
  if (value == null) return '';
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
    return String(value);
  }
  return JSON.stringify(value);
}

function flattenParsedFields(value: unknown, prefix = ''): Array<{ path: string; value: unknown }> {
  if (value == null || typeof value !== 'object') {
    return prefix ? [{ path: prefix, value }] : [];
  }
  if (Array.isArray(value)) {
    return value.flatMap((item, index) => flattenParsedFields(item, prefix ? `${prefix}.${index}` : String(index)));
  }
  return Object.entries(value as Record<string, unknown>).flatMap(([key, item]) => {
    const nextPath = prefix ? `${prefix}.${key}` : key;
    if (item != null && typeof item === 'object') {
      return flattenParsedFields(item, nextPath);
    }
    return [{ path: nextPath, value: item }];
  });
}

function exportFileName(prefix: string, filters: HistoryFilters) {
  const keyword = filters.keyword?.trim() || 'all';
  const protocol = filters.protocol?.trim() || 'all-source';
  return `${prefix}-${keyword}-${protocol}.csv`;
}

function trajectorySummaryText({
  filters,
  totalPoints,
  validPointCount,
  mileageDelta,
  maxSpeed,
  firstLocation,
  lastLocation
}: {
  filters: HistoryFilters;
  totalPoints: number;
  validPointCount: number;
  mileageDelta?: number;
  maxSpeed?: number;
  firstLocation?: HistoryLocationRow;
  lastLocation?: HistoryLocationRow;
}) {
  const vehicle = filters.keyword?.trim() || '全部车辆';
  const protocol = filters.protocol?.trim() || '全部来源';
  const dateFrom = filters.dateFrom?.trim();
  const dateTo = filters.dateTo?.trim();
  const range = dateFrom || dateTo ? `${dateFrom || '-'} 至 ${dateTo || '-'}` : '全部时间';
  const startTime = firstLocation?.deviceTime || firstLocation?.serverTime || '-';
  const endTime = lastLocation?.deviceTime || lastLocation?.serverTime || '-';
  return [
    '【轨迹回放摘要】',
    `车辆：${vehicle}`,
    `数据来源：${protocol}`,
    `查询范围：${range}`,
    `轨迹点：${totalPoints.toLocaleString()}，有效定位：${validPointCount.toLocaleString()}`,
    `区间里程：${formatNumber(mileageDelta, ' km')}`,
    `最高速度：${formatNumber(maxSpeed, ' km/h')}`,
    `起点：${startTime}`,
    `终点：${endTime}`,
    `当前位置服务：${window.location.origin}${window.location.pathname}${window.location.hash}`
  ].join('\n');
}

function appURL(hash: string) {
  return `${window.location.origin}${window.location.pathname}${hash}`;
}

function historyEvidencePackageText({
  filters,
  locationCount,
  rawCount,
  fieldCount,
  validPointCount,
  mileageDelta,
  maxSpeed,
  firstLocation,
  lastLocation,
  anomalySummary
}: {
  filters: HistoryFilters;
  locationCount: number;
  rawCount: number;
  fieldCount: number;
  validPointCount: number;
  mileageDelta?: number;
  maxSpeed?: number;
  firstLocation?: HistoryLocationRow;
  lastLocation?: HistoryLocationRow;
  anomalySummary: TrajectoryAnomalySummary;
}) {
  const vehicle = filters.keyword?.trim() || '';
  const protocol = filters.protocol?.trim() || '';
  const evidenceFilters = {
    ...(filters.dateFrom?.trim() ? { dateFrom: filters.dateFrom.trim() } : {}),
    ...(filters.dateTo?.trim() ? { dateTo: filters.dateTo.trim() } : {})
  };
  return [
    '【历史证据包】',
    `车辆：${vehicle || '全部车辆'}`,
    `数据来源：${protocol || '全部来源'}`,
    `查询范围：${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}`,
    `位置记录：${locationCount.toLocaleString()}，有效定位：${validPointCount.toLocaleString()}`,
    `RAW帧：${rawCount.toLocaleString()}，解析字段：${fieldCount.toLocaleString()}`,
    `区间里程：${formatNumber(mileageDelta, ' km')}，最高速度：${formatNumber(maxSpeed, ' km/h')}`,
    `起点：${firstLocation?.deviceTime || firstLocation?.serverTime || '-'}`,
    `终点：${lastLocation?.deviceTime || lastLocation?.serverTime || '-'}`,
    `异常：断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`,
    `轨迹回放：${appURL(buildAppHash({ page: 'history', keyword: vehicle, protocol, filters: evidenceFilters }))}`,
    `历史位置：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'location' } }))}`,
    `RAW证据：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`,
    `解析字段：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'fields', includeFields: 'true', ...(filters.fields?.trim() ? { fields: filters.fields.trim() } : {}) } }))}`,
    `里程复核：${appURL(buildAppHash({ page: 'mileage', keyword: vehicle, protocol, filters: evidenceFilters }))}`,
    `车辆服务：${appURL(buildAppHash({ page: 'detail', keyword: vehicle, protocol }))}`
  ].join('\n');
}

function trajectoryReviewPackageText({
  filters,
  locationCount,
  rawCount,
  fieldCount,
  validPointCount,
  coverageRate,
  playbackSpanMinutes,
  playbackIntervalMinutes,
  mileageDelta,
  maxSpeed,
  firstLocation,
  lastLocation,
  currentPlayback,
  currentPlaybackIndex,
  playbackCount,
  anomalySummary
}: {
  filters: HistoryFilters;
  locationCount: number;
  rawCount: number;
  fieldCount: number;
  validPointCount: number;
  coverageRate?: number;
  playbackSpanMinutes?: number;
  playbackIntervalMinutes?: number;
  mileageDelta?: number;
  maxSpeed?: number;
  firstLocation?: HistoryLocationRow;
  lastLocation?: HistoryLocationRow;
  currentPlayback?: HistoryLocationRow;
  currentPlaybackIndex: number;
  playbackCount: number;
  anomalySummary: TrajectoryAnomalySummary;
}) {
  const vehicle = filters.keyword?.trim() || '';
  const protocol = filters.protocol?.trim() || '';
  const evidenceFilters = {
    ...(filters.dateFrom?.trim() ? { dateFrom: filters.dateFrom.trim() } : {}),
    ...(filters.dateTo?.trim() ? { dateTo: filters.dateTo.trim() } : {})
  };
  const anomalyAction = anomalySummary.gapCount > 0 || anomalySummary.mileageRollbackCount > 0 || anomalySummary.overspeedCount > 0
    ? '建议核对 RAW 帧、解析字段、当日里程统计，并确认平台是否存在断链或补发。'
    : '当前页未发现明显断点、里程回退或速度异常，可作为轨迹证据使用。';
  return [
    '【轨迹复盘交接包】',
    `车辆：${vehicle || '全部车辆'}`,
    `数据来源：${protocol || '全部来源'}`,
    `查询范围：${filters.dateFrom?.trim() || '-'} 至 ${filters.dateTo?.trim() || '-'}`,
    `轨迹规模：位置 ${locationCount.toLocaleString()} / 有效定位 ${validPointCount.toLocaleString()} / RAW ${rawCount.toLocaleString()} / 解析字段 ${fieldCount.toLocaleString()}`,
    `轨迹质量：定位覆盖率 ${formatPercent(coverageRate)} / 回放跨度 ${formatDurationMinutes(playbackSpanMinutes)} / 采样间隔 ${formatDurationMinutes(playbackIntervalMinutes, '/点')}`,
    `里程速度：区间里程 ${formatNumber(mileageDelta, ' km')} / 最高速度 ${formatNumber(maxSpeed, ' km/h')}`,
    `起点：${firstLocation?.deviceTime || firstLocation?.serverTime || '-'}`,
    `终点：${lastLocation?.deviceTime || lastLocation?.serverTime || '-'}`,
    `当前回放点：${currentPlayback ? `点 ${currentPlaybackIndex + 1}/${playbackCount}，${currentPlayback.deviceTime || currentPlayback.serverTime || '-'}，${formatNumber(currentPlayback.speedKmh, ' km/h')}，${formatNumber(currentPlayback.totalMileageKm, ' km')}` : '-'}`,
    `异常判读：断点 ${anomalySummary.gapCount.toLocaleString()} / 里程回退 ${anomalySummary.mileageRollbackCount.toLocaleString()} / 超速 ${anomalySummary.overspeedCount.toLocaleString()}`,
    `最大断点：${formatDurationMinutes(anomalySummary.maxGapMinutes)}`,
    `最大里程回退：${formatNumber(anomalySummary.maxMileageRollbackKm, ' km')}`,
    `最高异常速度：${formatNumber(anomalySummary.maxSpeedKmh, ' km/h')}`,
    `下一步：${anomalyAction}`,
    `轨迹回放：${appURL(buildAppHash({ page: 'history', keyword: vehicle, protocol, filters: evidenceFilters }))}`,
    `历史位置：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'location' } }))}`,
    `RAW证据：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`,
    `解析字段：${appURL(buildAppHash({ page: 'history-query', keyword: vehicle, protocol, filters: { ...evidenceFilters, tab: 'fields', includeFields: 'true', ...(filters.fields?.trim() ? { fields: filters.fields.trim() } : {}) } }))}`,
    `里程复核：${appURL(buildAppHash({ page: 'mileage', keyword: vehicle, protocol, filters: evidenceFilters }))}`,
    `车辆服务：${appURL(buildAppHash({ page: 'detail', keyword: vehicle, protocol }))}`
  ].join('\n');
}

function amapLocationName(row: HistoryLocationRow, fallback: string) {
  return encodeURIComponent(row.plate || row.vin || fallback);
}

function amapTrajectoryURL(points: HistoryLocationRow[]) {
  const validPoints = points.filter(hasValidCoordinate);
  if (validPoints.length === 0) return '';
  if (validPoints.length === 1) {
    const point = validPoints[0];
    return `https://uri.amap.com/marker?position=${point.longitude},${point.latitude}&name=${amapLocationName(point, '车辆位置')}&src=lingniu-vehicle-platform`;
  }
  const start = validPoints[0];
  const end = validPoints[validPoints.length - 1];
  return [
    'https://uri.amap.com/navigation?',
    `from=${start.longitude},${start.latitude},${amapLocationName(start, '轨迹起点')}`,
    `&to=${end.longitude},${end.latitude},${amapLocationName(end, '轨迹终点')}`,
    '&mode=car',
    '&policy=1',
    '&src=lingniu-vehicle-platform'
  ].join('');
}

async function copyText(value: string, label: string) {
  const text = value.trim();
  if (!text) {
    Toast.warning(`${label}为空`);
    return;
  }
  try {
    await navigator.clipboard.writeText(text);
    Toast.success(`已复制${label}`);
  } catch {
    Toast.error(`复制${label}失败`);
  }
}

export function History({
  mode = 'trajectory',
  initialVin,
  initialProtocol,
  initialTab,
  initialFilters = {},
  onFiltersChange,
  onOpenVehicle,
  onOpenMileage,
  onOpenRaw
}: {
  mode?: 'trajectory' | 'query';
  initialVin: string;
  initialProtocol?: string;
  initialTab?: string;
  initialFilters?: Record<string, string>;
  onFiltersChange?: (filters: HistoryFilters, tab?: HistoryTabKey) => void;
  onOpenVehicle: (vin: string, protocol?: string) => void;
  onOpenMileage?: (filters: Record<string, string>) => void;
  onOpenRaw?: (filters: Record<string, string>) => void;
}) {
  const initialHistoryFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters);
  const [filters, setFilters] = useState<HistoryFilters>(initialHistoryFilters);
  const [activeTab, setActiveTab] = useState<HistoryTabKey>(normalizeHistoryTab(initialTab));
  const [locations, setLocations] = useState<Page<HistoryLocationRow>>(defaultPage);
  const [rawFrames, setRawFrames] = useState<Page<RawFrameRow>>(defaultPage);
  const [selectedRaw, setSelectedRaw] = useState<RawFrameRow | null>(null);
  const [loadingLocations, setLoadingLocations] = useState(false);
  const [loadingRaw, setLoadingRaw] = useState(false);
  const [locationPagination, setLocationPagination] = useState({ currentPage: 1, pageSize: 10 });
  const [rawPagination, setRawPagination] = useState({ currentPage: 1, pageSize: 10 });
  const [playbackIndex, setPlaybackIndex] = useState(0);
  const [playbackPlaying, setPlaybackPlaying] = useState(false);
  const [playbackSpeedMs, setPlaybackSpeedMs] = useState(1200);

  const buildParams = (nextFilters: HistoryFilters, limit: number, offset: number, raw: boolean) => {
    const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
    if (nextFilters.keyword?.trim()) params.set('keyword', nextFilters.keyword.trim());
    if (nextFilters.protocol?.trim()) params.set('protocol', nextFilters.protocol.trim());
    if (nextFilters.dateFrom?.trim()) params.set('dateFrom', nextFilters.dateFrom.trim());
    if (nextFilters.dateTo?.trim()) params.set('dateTo', nextFilters.dateTo.trim());
    if (raw && isIncludeFieldsEnabled(nextFilters.includeFields)) params.set('includeFields', 'true');
    if (raw && nextFilters.fields?.trim()) params.set('fields', nextFilters.fields.trim());
    return params;
  };

  const buildRawQuery = (nextFilters: HistoryFilters, limit: number, offset: number, forceIncludeFields = false): RawFrameQuery => {
    const query: RawFrameQuery = { limit, offset };
    if (nextFilters.keyword?.trim()) query.keyword = nextFilters.keyword.trim();
    if (nextFilters.protocol?.trim()) query.protocol = nextFilters.protocol.trim();
    if (nextFilters.dateFrom?.trim()) query.dateFrom = nextFilters.dateFrom.trim();
    if (nextFilters.dateTo?.trim()) query.dateTo = nextFilters.dateTo.trim();
    if (forceIncludeFields || isIncludeFieldsEnabled(nextFilters.includeFields)) query.includeFields = true;
    const fields = splitFields(nextFilters.fields);
    if (fields.length > 0) query.fields = fields;
    return query;
  };

  const loadLocations = (nextFilters = filters, page = locationPagination.currentPage, pageSize = locationPagination.pageSize) => {
    setLoadingLocations(true);
    api.historyLocations(buildParams(nextFilters, pageSize, (page - 1) * pageSize, false))
      .then((nextPage) => {
        setLocations(nextPage);
        setLocationPagination({ currentPage: page, pageSize });
        setPlaybackIndex(0);
        setPlaybackPlaying(false);
      })
      .catch((error: Error) => Toast.error(error.message))
      .finally(() => setLoadingLocations(false));
  };

  const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize, forceIncludeFields = activeTab === 'fields') => {
    if ((forceIncludeFields || isIncludeFieldsEnabled(nextFilters.includeFields)) && shouldBlockRawFieldQuery(nextFilters)) {
      setRawFrames({ items: [], total: 0, limit: pageSize, offset: (page - 1) * pageSize });
      setRawPagination({ currentPage: page, pageSize });
      Toast.warning('RAW 解析字段查询需要车辆、时间范围或字段裁剪');
      return;
    }
    setLoadingRaw(true);
    api.rawFramesQuery(buildRawQuery(nextFilters, pageSize, (page - 1) * pageSize, forceIncludeFields))
      .then((nextPage) => {
        setRawFrames(nextPage);
        setRawPagination({ currentPage: page, pageSize });
      })
      .catch((error: Error) => Toast.error(error.message))
      .finally(() => setLoadingRaw(false));
  };

  const submit = (values: Record<string, unknown>) => {
    const nextFilters: HistoryFilters = {
      keyword: String(values.keyword ?? ''),
      protocol: String(values.protocol ?? ''),
      dateFrom: String(values.dateFrom ?? ''),
      dateTo: String(values.dateTo ?? ''),
      fields: String(values.fields ?? ''),
      includeFields: Boolean(values.includeFields)
    };
    setFilters(nextFilters);
    onFiltersChange?.(nextFilters, activeTab);
    loadLocations(nextFilters, 1, locationPagination.pageSize);
    loadRawFrames(nextFilters, 1, rawPagination.pageSize, activeTab === 'fields');
  };

  const applyFilters = (nextFilters: HistoryFilters) => {
    setFilters(nextFilters);
    onFiltersChange?.(nextFilters, activeTab);
    loadLocations(nextFilters, 1, locationPagination.pageSize);
    loadRawFrames(nextFilters, 1, rawPagination.pageSize, activeTab === 'fields');
  };

  const reset = () => {
    const nextFilters = mergeInitialFilters(initialVin, initialProtocol, {});
    applyFilters(nextFilters);
  };

  useEffect(() => {
    const nextFilters = mergeInitialFilters(initialVin, initialProtocol, initialFilters);
    const nextTab = normalizeHistoryTab(initialTab);
    setFilters(nextFilters);
    setActiveTab(nextTab);
    loadLocations(nextFilters, 1, locationPagination.pageSize);
    loadRawFrames(nextFilters, 1, rawPagination.pageSize, nextTab === 'fields');
  }, [initialVin, initialProtocol, initialTab, JSON.stringify(initialFilters)]);

  const changeTab = (key: string) => {
    const nextTab = normalizeHistoryTab(String(key));
    const nextFilters = nextTab === 'fields' ? { ...filters, includeFields: true } : filters;
    setActiveTab(nextTab);
    setFilters(nextFilters);
    onFiltersChange?.(nextFilters, nextTab);
    if (nextTab === 'fields') {
      loadRawFrames(nextFilters, 1, rawPagination.pageSize, true);
    }
  };

  const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]);
  const rawItems = rawFrames.items ?? [];
  const rawFieldRows = useMemo<RawFieldRow[]>(() => rawItems.flatMap((row) => {
    const fields = flattenParsedFields(row.parsedFields ?? {});
    return fields.map((field, index) => ({
      id: `${row.id}-${field.path}-${index}`,
      rawId: row.id,
      vin: row.vin,
      plate: row.plate,
      protocol: row.protocol,
      frameType: row.frameType,
      deviceTime: row.deviceTime,
      serverTime: row.serverTime,
      fieldPath: field.path,
      fieldValue: field.value
    }));
  }), [rawItems]);
  const locationItems = locations.items ?? [];
  const validLocations = locationItems.filter(hasValidCoordinate);
  const mileageValues = locationItems.map((item) => item.totalMileageKm).filter(isFiniteNumber);
  const mileageDelta = mileageValues.length > 1 ? Math.max(...mileageValues) - Math.min(...mileageValues) : undefined;
  const maxSpeed = locationItems.map((item) => item.speedKmh).filter(isFiniteNumber).reduce<number | undefined>((max, item) => max == null ? item : Math.max(max, item), undefined);
  const firstLocation = locationItems[0];
  const lastLocation = locationItems[locationItems.length - 1];
  const trajectoryCoverageRate = locationItems.length > 0 ? (validLocations.length / locationItems.length) * 100 : undefined;
  const playbackStartMs = timeValue(firstLocation);
  const playbackEndMs = timeValue(lastLocation);
  const playbackSpanMinutes = playbackStartMs != null && playbackEndMs != null ? Math.abs(playbackEndMs - playbackStartMs) / 60000 : undefined;
  const playbackIntervalMinutes = isFiniteNumber(playbackSpanMinutes) && locationItems.length > 1 ? playbackSpanMinutes / (locationItems.length - 1) : undefined;
  const trajectoryAnomalies = useMemo(() => analyzeTrajectoryAnomalies(locationItems), [locationItems]);
  const hasTrajectoryAnomaly = trajectoryAnomalies.gapCount > 0 || trajectoryAnomalies.mileageRollbackCount > 0 || trajectoryAnomalies.overspeedCount > 0;
  const currentVehicleKeyword = filters.keyword?.trim() ?? '';
  const currentProtocol = filters.protocol?.trim() ?? '';
  const pageTitle = mode === 'query' ? '历史查询' : '轨迹回放';
  const pageDescription = mode === 'query'
    ? '按车辆查询历史位置、RAW 帧和扁平解析字段，支持分页、字段裁剪和证据导出'
    : '按车辆查询历史位置、轨迹回放和 RAW 帧证据，数据来源只作为过滤和诊断维度';
  const scopeDescription = mode === 'query'
    ? '历史位置、RAW 帧和解析字段按当前车辆与来源范围分页查询。'
    : '历史位置和 RAW 帧按当前车辆与来源范围查询。';
  const amapConfigured = isAMapConfigured();
  const selectedFieldCount = splitFields(filters.fields).length;
  const playbackRows = validLocations.length > 0 ? validLocations : locationItems;
  const currentPlaybackIndex = playbackRows.length === 0 ? -1 : Math.min(playbackIndex, playbackRows.length - 1);
  const currentPlayback = currentPlaybackIndex >= 0 ? playbackRows[currentPlaybackIndex] : undefined;
  const currentPlaybackPointId = currentPlayback ? playbackPointId(currentPlayback, currentPlaybackIndex) : undefined;
  const playbackAtEnd = playbackRows.length > 0 && currentPlaybackIndex >= playbackRows.length - 1;
  const playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({
    id: playbackPointId(row, index),
    label: row.plate || row.vin || `点 ${index + 1}`,
    longitude: row.longitude,
    latitude: row.latitude,
    online: true,
    title: `${row.deviceTime || row.serverTime || '-'} ${row.speedKmh ?? '-'} km/h`
  }));
  const filterSummary = [
    currentVehicleKeyword ? `车辆：${currentVehicleKeyword}` : '',
    currentProtocol ? `数据来源：${currentProtocol}` : '',
    filters.dateFrom?.trim() ? `开始时间：${filters.dateFrom.trim()}` : '',
    filters.dateTo?.trim() ? `结束时间：${filters.dateTo.trim()}` : '',
    isIncludeFieldsEnabled(filters.includeFields) ? '返回解析字段' : '',
    selectedFieldCount > 0 ? `字段裁剪：${selectedFieldCount} 个` : ''
  ].filter(Boolean);
  const clearRangeFilters = () => {
    applyFilters(currentVehicleKeyword ? { keyword: currentVehicleKeyword } : {});
  };
  const exportLocations = () => {
    if (locations.items.length === 0) {
      Toast.warning('当前没有可导出的位置历史');
      return;
    }
    downloadCsv(exportFileName('history-locations', filters), buildCsv(locationExportColumns, locations.items));
    Toast.success(`已导出 ${locations.items.length} 条位置历史`);
  };
  const exportRawFrames = () => {
    if (rawFrames.items.length === 0) {
      Toast.warning('当前没有可导出的 RAW 帧');
      return;
    }
    downloadCsv(exportFileName('raw-frames', filters), buildCsv(rawExportColumns, rawFrames.items));
    Toast.success(`已导出 ${rawFrames.items.length} 条 RAW 帧`);
  };
  const exportRawFields = () => {
    if (rawFieldRows.length === 0) {
      Toast.warning('当前没有可导出的解析字段');
      return;
    }
    downloadCsv(exportFileName('raw-fields', filters), buildCsv(rawFieldExportColumns, rawFieldRows));
    Toast.success(`已导出 ${rawFieldRows.length} 条解析字段`);
  };
  const copyTrajectorySummary = () => {
    copyText(
      trajectorySummaryText({
        filters,
        totalPoints: locations.total,
        validPointCount: validLocations.length,
        mileageDelta,
        maxSpeed,
        firstLocation,
        lastLocation
      }),
      '轨迹摘要'
    );
  };
  const copyHistoryEvidencePackage = () => {
    copyText(
      historyEvidencePackageText({
        filters,
        locationCount: locations.total ?? 0,
        rawCount: rawFrames.total ?? 0,
        fieldCount: rawFieldRows.length,
        validPointCount: validLocations.length,
        mileageDelta,
        maxSpeed,
        firstLocation,
        lastLocation,
        anomalySummary: trajectoryAnomalies
      }),
      '历史证据包'
    );
  };
  const copyTrajectoryReviewPackage = () => {
    copyText(
      trajectoryReviewPackageText({
        filters,
        locationCount: locations.total ?? 0,
        rawCount: rawFrames.total ?? 0,
        fieldCount: rawFieldRows.length,
        validPointCount: validLocations.length,
        coverageRate: trajectoryCoverageRate,
        playbackSpanMinutes,
        playbackIntervalMinutes,
        mileageDelta,
        maxSpeed,
        firstLocation,
        lastLocation,
        currentPlayback,
        currentPlaybackIndex,
        playbackCount: playbackRows.length,
        anomalySummary: trajectoryAnomalies
      }),
      '轨迹复盘交接包'
    );
  };
  const openAmapTrajectory = () => {
    const url = amapTrajectoryURL(validLocations);
    if (!url) {
      Toast.warning('当前轨迹没有有效坐标');
      return;
    }
    window.open(url, '_blank', 'noopener,noreferrer');
  };
  useEffect(() => {
    if (!playbackPlaying || playbackRows.length <= 1) return undefined;
    const timer = window.setInterval(() => {
      setPlaybackIndex((current) => {
        if (current >= playbackRows.length - 1) {
          setPlaybackPlaying(false);
          return current;
        }
        const next = current + 1;
        if (next >= playbackRows.length - 1) setPlaybackPlaying(false);
        return next;
      });
    }, playbackSpeedMs);
    return () => window.clearInterval(timer);
  }, [playbackPlaying, playbackRows.length, playbackSpeedMs]);
  const togglePlayback = () => {
    if (playbackPlaying) {
      setPlaybackPlaying(false);
      return;
    }
    if (playbackRows.length <= 1) return;
    if (playbackAtEnd) setPlaybackIndex(0);
    setPlaybackPlaying(true);
  };
  const movePlayback = (delta: number) => {
    if (playbackRows.length === 0) return;
    setPlaybackPlaying(false);
    setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1));
  };
  const selectPlaybackPoint = (nextIndex: number) => {
    if (playbackRows.length === 0) return;
    setPlaybackPlaying(false);
    setPlaybackIndex(Math.min(Math.max(nextIndex, 0), playbackRows.length - 1));
  };
  const selectPlaybackMapPoint = (point: VehicleMapPoint) => {
    const index = playbackRows.findIndex((row, rowIndex) => playbackPointId(row, rowIndex) === point.id);
    if (index >= 0) {
      selectPlaybackPoint(index);
    }
  };
  const openPlaybackVehicle = () => {
    if (!currentPlayback || !canOpenVehicle(currentPlayback.vin)) return;
    onOpenVehicle(currentPlayback.vin, currentPlayback.protocol);
  };
  const openLocationMileage = (row: HistoryLocationRow) => {
    if (!canOpenVehicle(row.vin)) return;
    const day = dateOnly(row.deviceTime || row.serverTime || row.lastSeen);
    onOpenMileage?.({
      keyword: row.vin,
      protocol: row.protocol,
      ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {})
    });
  };
  const openLocationRaw = (row: HistoryLocationRow) => {
    if (!canOpenVehicle(row.vin)) return;
    const day = dateOnly(row.deviceTime || row.serverTime || row.lastSeen);
    onOpenRaw?.({
      keyword: row.vin,
      protocol: row.protocol,
      ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {}),
      includeFields: 'true'
    });
  };
  const openRawMileage = (row: RawFrameRow) => {
    if (!canOpenVehicle(row.vin)) return;
    const day = dateOnly(row.deviceTime || row.serverTime);
    onOpenMileage?.({
      keyword: row.vin,
      protocol: row.protocol,
      ...(day ? { dateFrom: day, dateTo: nextDate(day) } : {})
    });
  };

  return (
    <div className="vp-page">
      <PageHeader
        title={pageTitle}
        description={pageDescription}
        actions={(
          <Space>
            <Button icon={<IconCopy />} onClick={copyHistoryEvidencePackage}>复制历史证据包</Button>
            <Button disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}>
              当前车辆服务
            </Button>
          </Space>
        )}
      />
      <div className="vp-scope-bar">
        <span className="vp-scope-label">当前车辆：{currentVehicleKeyword || '-'}</span>
        <Tag color={currentProtocol ? 'blue' : 'green'}>当前来源：{currentProtocol || '全部来源'}</Tag>
        <Typography.Text type="tertiary">{scopeDescription}</Typography.Text>
      </div>
      <Card bordered>
        <Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => submit(values)}>
          <Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号" style={{ width: 260 }} />
          <Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 190 }}>
            <Select.Option value="GB32960">GB32960</Select.Option>
            <Select.Option value="JT808">JT808</Select.Option>
            <Select.Option value="YUTONG_MQTT">YUTONG_MQTT</Select.Option>
          </Form.Select>
          <Form.Input field="dateFrom" label="开始时间" placeholder="2026-07-03 00:00:00" style={{ width: 210 }} />
          <Form.Input field="dateTo" label="结束时间" placeholder="2026-07-03 23:59:59" style={{ width: 210 }} />
          <Form.Checkbox field="includeFields" noLabel>
            返回解析字段
          </Form.Checkbox>
          <Form.TextArea
            field="fields"
            label="字段裁剪"
            placeholder="可选，逗号分隔，如 jt808.header.phone,jt808.location.longitude"
            autosize={{ minRows: 1, maxRows: 3 }}
            style={{ width: 420 }}
          />
          <Space align="start">
            <Button icon={<IconSearch />} htmlType="submit" theme="solid" type="primary">查询</Button>
            <Button icon={<IconRefresh />} onClick={reset}>重置</Button>
          </Space>
        </Form>
      </Card>
      {filterSummary.length > 0 ? (
        <Card bordered title="当前历史筛选" style={{ marginTop: 16 }}>
          <Space wrap>
            {filterSummary.map((item) => (
              <Tag key={item} color="blue">{item}</Tag>
            ))}
            <Button size="small" onClick={clearRangeFilters}>清空筛选</Button>
          </Space>
        </Card>
      ) : null}
      <Card
        bordered
        title={(
          <Space>
            <span>轨迹回放作业台</span>
            <Button size="small" theme="light" icon={<IconCopy />} onClick={copyTrajectorySummary}>复制轨迹摘要</Button>
            <Button size="small" theme="light" icon={<IconCopy />} onClick={copyHistoryEvidencePackage}>复制历史证据包</Button>
            <Button size="small" theme="light" icon={<IconCopy />} onClick={copyTrajectoryReviewPackage}>复制轨迹复盘包</Button>
            <Button size="small" theme="light" disabled={validLocations.length === 0} onClick={openAmapTrajectory}>高德线路</Button>
          </Space>
        )}
        style={{ marginTop: 16 }}
      >
        <div className="vp-playback-layout">
          <div className="vp-playback-map">
            <div className="vp-monitor-map-header">
              <Space wrap>
                <Tag color="blue">{validLocations.length.toLocaleString()} 个有效轨迹点</Tag>
                <Tag color={currentProtocol ? 'blue' : 'green'}>{currentProtocol || '全部来源'}</Tag>
                <Tag color="green">{formatNumber(mileageDelta, ' km')}</Tag>
                <Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '高德地图配置就绪' : '高德地图待配置'}</Tag>
              </Space>
            </div>
            <VehicleMap
              points={playbackPoints}
              mode="track"
              selectedId={currentPlaybackPointId}
              onPointSelect={selectPlaybackMapPoint}
              fallbackLabel="高德地图未配置，显示轨迹坐标预览"
            />
          </div>
          <div className="vp-playback-side">
            {[
              { label: '轨迹点', value: locations.total.toLocaleString(), color: 'blue' as const },
              { label: '有效定位', value: validLocations.length.toLocaleString(), color: 'green' as const },
              { label: '区间里程', value: formatNumber(mileageDelta, ' km'), color: 'green' as const },
              { label: '最高速度', value: formatNumber(maxSpeed, ' km/h'), color: 'orange' as const }
            ].map((item) => (
              <div key={item.label} className="vp-monitor-metric">
                <Tag color={item.color}>{item.label}</Tag>
                <div className="vp-monitor-metric-value">{item.value}</div>
              </div>
            ))}
          <div className="vp-playback-current">
              <div className="vp-map-service-queue-title">当前回放点</div>
              {currentPlayback ? (
                <>
                  <Space wrap>
                    <Tag color="blue">点 {currentPlaybackIndex + 1} / {playbackRows.length}</Tag>
                    <Tag color={currentPlayback.protocol ? 'blue' : 'grey'}>{currentPlayback.protocol || '-'}</Tag>
                  </Space>
                  <Typography.Text strong>{currentPlayback.plate || currentPlayback.vin}</Typography.Text>
                  <Typography.Text type="tertiary" size="small">{currentPlayback.deviceTime || currentPlayback.serverTime || '-'}</Typography.Text>
                  <Space wrap>
                    <Tag color="green">{formatNumber(currentPlayback.speedKmh, ' km/h')}</Tag>
                    <Tag color="blue">{formatNumber(currentPlayback.totalMileageKm, ' km')}</Tag>
                  </Space>
                  <Space wrap>
                    <Button
                      size="small"
                      aria-label={playbackPlaying ? '暂停轨迹' : '播放轨迹'}
                      disabled={playbackRows.length <= 1}
                      onClick={togglePlayback}
                    >
                      {playbackPlaying ? '暂停' : playbackAtEnd ? '重播' : '播放'}
                    </Button>
                    <Typography.Text type="secondary" size="small">播放速度</Typography.Text>
                    <Select
                      size="small"
                      value={String(playbackSpeedMs)}
                      style={{ width: 108 }}
                      onChange={(value) => setPlaybackSpeedMs(Number(value))}
                    >
                      <Select.Option value="1800">慢速</Select.Option>
                      <Select.Option value="1200">标准</Select.Option>
                      <Select.Option value="600">快速</Select.Option>
                    </Select>
                  </Space>
                  <Space wrap>
                    <Button size="small" disabled={currentPlaybackIndex <= 0} onClick={() => movePlayback(-1)}>上一点</Button>
                    <Button size="small" disabled={currentPlaybackIndex >= playbackRows.length - 1} onClick={() => movePlayback(1)}>下一点</Button>
                    <Button size="small" disabled={!canOpenVehicle(currentPlayback.vin)} onClick={openPlaybackVehicle}>回放点车辆服务</Button>
                  </Space>
                  <input
                    className="vp-playback-range"
                    aria-label="轨迹回放进度"
                    type="range"
                    min={0}
                    max={Math.max(playbackRows.length - 1, 0)}
                    step={1}
                    value={currentPlaybackIndex}
                    onChange={(event) => selectPlaybackPoint(Number(event.currentTarget.value))}
                  />
                </>
              ) : (
                <Typography.Text type="tertiary">暂无可回放位置点</Typography.Text>
              )}
            </div>
          </div>
        </div>
        <div className="vp-playback-timeline">
          {(validLocations.length > 0 ? validLocations : locationItems).slice(0, 8).map((row, index) => (
            <button
              key={`${row.deviceTime}-${index}`}
              type="button"
              className={`vp-playback-step ${index === currentPlaybackIndex ? 'vp-playback-step-active' : ''}`}
              aria-label={`选择轨迹点 ${index + 1}`}
              onClick={() => selectPlaybackPoint(index)}
            >
              <Tag color={index === 0 ? 'green' : index === Math.min((validLocations.length || locationItems.length), 8) - 1 ? 'orange' : 'blue'}>
                {index === 0 ? '起点' : index === Math.min((validLocations.length || locationItems.length), 8) - 1 ? '终点' : `点 ${index + 1}`}
              </Tag>
              <div className="vp-evidence-value">{row.deviceTime || row.serverTime || '-'}</div>
              <Typography.Text type="secondary">
                {formatNumber(row.speedKmh, ' km/h')} / {formatNumber(row.totalMileageKm, ' km')}
              </Typography.Text>
            </button>
          ))}
          {locationItems.length === 0 ? (
            <Typography.Text type="secondary">当前筛选范围暂无轨迹点，请调整车辆、来源或时间范围。</Typography.Text>
          ) : null}
        </div>
        <Card bordered title="轨迹证据质量" style={{ marginTop: 12 }}>
          <div className="vp-stat-insight-grid">
            {[
              {
                label: '定位覆盖率',
                value: formatPercent(trajectoryCoverageRate),
                color: (trajectoryCoverageRate ?? 0) >= 80 ? 'green' as const : 'orange' as const,
                detail: `${validLocations.length.toLocaleString()} / ${locationItems.length.toLocaleString()} 个点有有效坐标。`
              },
              {
                label: '回放跨度',
                value: formatDurationMinutes(playbackSpanMinutes),
                color: isFiniteNumber(playbackSpanMinutes) ? 'blue' as const : 'grey' as const,
                detail: '当前页首末轨迹点设备时间跨度。'
              },
              {
                label: '采样间隔',
                value: formatDurationMinutes(playbackIntervalMinutes, '/点'),
                color: isFiniteNumber(playbackIntervalMinutes) && playbackIntervalMinutes <= 10 ? 'green' as const : 'orange' as const,
                detail: '当前页估算的平均轨迹采样间隔。'
              }
            ].map((item) => (
              <Card key={item.label} bordered>
                <Tag color={item.color}>{item.label}</Tag>
                <div className="vp-monitor-metric-value">{item.value}</div>
                <Typography.Text type="secondary">{item.detail}</Typography.Text>
              </Card>
            ))}
          </div>
        </Card>
        <Card bordered title="轨迹异常判读" style={{ marginTop: 12 }}>
          <div className="vp-trajectory-anomaly-grid">
            {[
              {
                label: '数据断点',
                value: trajectoryAnomalies.gapCount > 0 ? `${trajectoryAnomalies.gapCount.toLocaleString()} 处` : '无',
                detail: trajectoryAnomalies.gapCount > 0 ? `最大断点 ${formatDurationMinutes(trajectoryAnomalies.maxGapMinutes)}` : '相邻轨迹点未发现超过 30 分钟断点。',
                color: trajectoryAnomalies.gapCount > 0 ? 'orange' as const : 'green' as const
              },
              {
                label: '里程回退',
                value: trajectoryAnomalies.mileageRollbackCount > 0 ? formatNumber(trajectoryAnomalies.maxMileageRollbackKm, ' km') : '无',
                detail: trajectoryAnomalies.mileageRollbackCount > 0 ? `${trajectoryAnomalies.mileageRollbackCount.toLocaleString()} 处总里程低于上一点。` : '当前页总里程单调不回退。',
                color: trajectoryAnomalies.mileageRollbackCount > 0 ? 'red' as const : 'green' as const
              },
              {
                label: '速度异常',
                value: trajectoryAnomalies.overspeedCount > 0 ? formatNumber(trajectoryAnomalies.maxSpeedKmh, ' km/h') : '无',
                detail: trajectoryAnomalies.overspeedCount > 0 ? `${trajectoryAnomalies.overspeedCount.toLocaleString()} 个点超过 120 km/h。` : '当前页未发现超过 120 km/h 的速度点。',
                color: trajectoryAnomalies.overspeedCount > 0 ? 'orange' as const : 'green' as const
              }
            ].map((item) => (
              <div key={item.label} className="vp-trajectory-anomaly-item">
                <Tag color={item.color}>{item.label}</Tag>
                <div className="vp-monitor-metric-value">{item.value}</div>
                <Typography.Text type="secondary">{item.detail}</Typography.Text>
              </div>
            ))}
          </div>
          <div className="vp-trajectory-anomaly-action">
            <Tag color={hasTrajectoryAnomaly ? 'orange' : 'green'}>{hasTrajectoryAnomaly ? '建议核对 RAW 帧和当日里程统计' : '轨迹质量可用'}</Tag>
            <Typography.Text type="secondary">
              {hasTrajectoryAnomaly ? '异常点会影响轨迹回放、定位复盘和区间里程判断，请结合 RAW 解析字段与里程统计交叉确认。' : '当前页轨迹未发现明显断点、里程回退或速度异常。'}
            </Typography.Text>
          </div>
        </Card>
        <Space wrap style={{ marginTop: 12 }}>
          <Tag color="grey">起点：{firstLocation?.deviceTime || firstLocation?.serverTime || '-'}</Tag>
          <Tag color="grey">终点：{lastLocation?.deviceTime || lastLocation?.serverTime || '-'}</Tag>
          <Tag color="grey">地图接入：高德 JS API 运行时配置</Tag>
        </Space>
      </Card>
      <Card bordered style={{ marginTop: 16 }}>
        <Tabs activeKey={activeTab} onChange={(key) => changeTab(String(key))}>
          <Tabs.TabPane tab="位置历史" itemKey="location">
            <div className="vp-table-toolbar">
              <Space wrap>
                <Tag color="blue">当前页 {locations.items.length.toLocaleString()} 条</Tag>
                <Button size="small" onClick={exportLocations}>导出位置当前页 CSV</Button>
              </Space>
            </div>
            <Table
              rowKey="deviceTime"
              dataSource={locations.items}
              loading={loadingLocations}
              pagination={{
                currentPage: locationPagination.currentPage,
                pageSize: locationPagination.pageSize,
                total: locations.total,
                showSizeChanger: true,
                onPageChange: (page) => loadLocations(filters, page, locationPagination.pageSize),
                onPageSizeChange: (pageSize) => loadLocations(filters, 1, pageSize)
              }}
              columns={[
                { title: 'VIN', dataIndex: 'vin', width: 190 },
                { title: '数据来源', dataIndex: 'protocol', width: 120 },
                { title: '经度', dataIndex: 'longitude', width: 120 },
                { title: '纬度', dataIndex: 'latitude', width: 120 },
                { title: '速度 km/h', dataIndex: 'speedKmh', width: 120 },
                { title: '总里程 km', dataIndex: 'totalMileageKm', width: 130 },
                { title: '设备时间', dataIndex: 'deviceTime', width: 190 },
                { title: '入库时间', dataIndex: 'serverTime', width: 190 },
                {
                  title: '操作',
                  width: 280,
                  render: (_: unknown, row: HistoryLocationRow) => (
                    <Space wrap>
                      <Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, row.protocol)}>车辆服务</Button>
                      <Button disabled={!canOpenVehicle(row.vin) || !onOpenMileage} onClick={() => openLocationMileage(row)}>核对里程</Button>
                      <Button disabled={!canOpenVehicle(row.vin) || !onOpenRaw} onClick={() => openLocationRaw(row)}>核对 RAW</Button>
                    </Space>
                  )
                }
              ]}
            />
          </Tabs.TabPane>
          <Tabs.TabPane tab="RAW 帧" itemKey="raw">
            <div className="vp-table-toolbar">
              <Space wrap>
                <Tag color="blue">当前页 {rawFrames.items.length.toLocaleString()} 条</Tag>
                <Tag color={isIncludeFieldsEnabled(filters.includeFields) || splitFields(filters.fields).length > 0 ? 'green' : 'orange'}>
                  {isIncludeFieldsEnabled(filters.includeFields) || splitFields(filters.fields).length > 0 ? '包含解析字段' : '未请求解析字段'}
                </Tag>
                <Button size="small" onClick={exportRawFrames}>导出 RAW 当前页 CSV</Button>
              </Space>
            </div>
            <Table
              rowKey="id"
              dataSource={rawFrames.items}
              loading={loadingRaw}
              pagination={{
                currentPage: rawPagination.currentPage,
                pageSize: rawPagination.pageSize,
                total: rawFrames.total,
                showSizeChanger: true,
                onPageChange: (page) => loadRawFrames(filters, page, rawPagination.pageSize),
                onPageSizeChange: (pageSize) => loadRawFrames(filters, 1, pageSize)
              }}
              columns={[
                { title: 'ID', dataIndex: 'id', width: 260 },
                { title: 'VIN', dataIndex: 'vin', width: 190 },
                { title: '车牌', dataIndex: 'plate', width: 120 },
                { title: '数据来源', dataIndex: 'protocol', width: 120 },
                { title: '帧类型', dataIndex: 'frameType', width: 190 },
                { title: '大小 B', dataIndex: 'rawSizeBytes', width: 100 },
                { title: '设备时间', dataIndex: 'deviceTime', width: 190 },
                { title: '入库时间', dataIndex: 'serverTime', width: 190 },
                {
                  title: '操作',
                  width: 280,
                  render: (_: unknown, row: RawFrameRow) => (
                    <Space wrap>
                      <Button onClick={() => setSelectedRaw(row)}>字段</Button>
                      <Button disabled={!canOpenVehicle(row.vin) || !onOpenMileage} onClick={() => openRawMileage(row)}>核对里程</Button>
                      <Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, row.protocol)}>车辆服务</Button>
                    </Space>
                  )
                }
              ]}
            />
          </Tabs.TabPane>
          <Tabs.TabPane tab="解析字段" itemKey="fields">
            <div className="vp-table-toolbar">
              <Space wrap>
                <Tag color="green">当前页 {rawFieldRows.length.toLocaleString()} 个字段</Tag>
                <Tag color="blue">{rawFrames.items.length.toLocaleString()} 条 RAW 来源</Tag>
                {selectedFieldCount > 0 ? <Tag color="blue">字段裁剪 {selectedFieldCount.toLocaleString()} 个</Tag> : <Tag color="grey">全量解析字段</Tag>}
                <Button size="small" onClick={exportRawFields}>导出解析字段当前页 CSV</Button>
              </Space>
            </div>
            <Table
              rowKey="id"
              dataSource={rawFieldRows}
              loading={loadingRaw}
              pagination={false}
              columns={[
                { title: '字段', dataIndex: 'fieldPath', width: 320 },
                {
                  title: '值',
                  width: 260,
                  render: (_: unknown, row: RawFieldRow) => (
                    <Typography.Text ellipsis={{ showTooltip: true }}>{formatFieldValue(row.fieldValue) || '-'}</Typography.Text>
                  )
                },
                { title: 'VIN', dataIndex: 'vin', width: 190 },
                { title: '车牌', dataIndex: 'plate', width: 120 },
                { title: '数据来源', dataIndex: 'protocol', width: 120 },
                { title: '帧类型', dataIndex: 'frameType', width: 190 },
                { title: '设备时间', dataIndex: 'deviceTime', width: 190 },
                { title: 'RAW ID', dataIndex: 'rawId', width: 260 },
                {
                  title: '操作',
                  width: 170,
                  render: (_: unknown, row: RawFieldRow) => (
                    <Space wrap>
                      <Button disabled={!canOpenVehicle(row.vin)} onClick={() => onOpenVehicle(row.vin, row.protocol)}>车辆服务</Button>
                    </Space>
                  )
                }
              ]}
            />
            <Space wrap style={{ marginTop: 12 }}>
              <Tag color="grey">分页沿用 RAW 帧分页</Tag>
              <Tag color="grey">字段来自 parsedFields，保留协议字段映射后的路径</Tag>
            </Space>
          </Tabs.TabPane>
        </Tabs>
      </Card>
      <SideSheet title="RAW 解析字段" visible={Boolean(selectedRaw)} onCancel={() => setSelectedRaw(null)} width={720}>
        <Space vertical align="start" spacing={12} style={{ width: '100%' }}>
          <Typography.Text type="tertiary">
            {selectedRaw?.protocol ?? '-'} / {selectedRaw?.plate || selectedRaw?.vin || '-'} / {rawFieldCount} 个字段
          </Typography.Text>
          {rawFieldCount === 0 ? (
            <Typography.Text type="secondary">当前查询未返回解析字段，请勾选“返回解析字段”或配置字段裁剪后重新查询。</Typography.Text>
          ) : null}
        </Space>
        <pre className="vp-json">{JSON.stringify(selectedRaw?.parsedFields ?? {}, null, 2)}</pre>
      </SideSheet>
    </div>
  );
}
