import { Button, Card, Form, Select, SideSheet, Space, Table, Tabs, Tag, Toast, Typography } from '@douyinfe/semi-ui';
import { 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 { buildCsv, downloadCsv, type CsvColumn } from '../domain/csvExport';

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

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';

function normalizeHistoryTab(value?: string): HistoryTabKey {
  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 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 ?? {} }
];

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

export function History({
  initialVin,
  initialProtocol,
  initialTab,
  initialFilters = {},
  onFiltersChange,
  onOpenVehicle,
  onOpenMileage,
  onOpenRaw
}: {
  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 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): 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 (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);
      })
      .catch((error: Error) => Toast.error(error.message))
      .finally(() => setLoadingLocations(false));
  };

  const loadRawFrames = (nextFilters = filters, page = rawPagination.currentPage, pageSize = rawPagination.pageSize) => {
    if (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))
      .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);
  };

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

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

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

  const changeTab = (key: string) => {
    const nextTab = normalizeHistoryTab(String(key));
    setActiveTab(nextTab);
    onFiltersChange?.(filters, nextTab);
  };

  const rawFieldCount = useMemo(() => Object.keys(selectedRaw?.parsedFields ?? {}).length, [selectedRaw]);
  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 currentVehicleKeyword = filters.keyword?.trim() ?? '';
  const currentProtocol = filters.protocol?.trim() ?? '';
  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 playbackPoints: VehicleMapPoint[] = validLocations.map((row, index) => ({
    id: `${row.vin}-${row.deviceTime || row.serverTime || 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 movePlayback = (delta: number) => {
    if (playbackRows.length === 0) return;
    setPlaybackIndex((current) => Math.min(Math.max(current + delta, 0), playbackRows.length - 1));
  };
  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="轨迹回放"
        description="按车辆查询历史位置、轨迹回放和 RAW 帧证据，数据来源只作为过滤和诊断维度"
        actions={(
          <Button disabled={!currentVehicleKeyword} onClick={() => onOpenVehicle(currentVehicleKeyword, currentProtocol)}>
            当前车辆服务
          </Button>
        )}
      />
      <div className="vp-scope-bar">
        <span className="vp-scope-label">当前车辆：{currentVehicleKeyword || '-'}</span>
        <Tag color={currentProtocol ? 'blue' : 'green'}>当前来源：{currentProtocol || '全部来源'}</Tag>
        <Typography.Text type="tertiary">历史位置和 RAW 帧按当前车辆与来源范围查询。</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="轨迹回放作业台" 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"
              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" 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>
                </>
              ) : (
                <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) => (
            <div key={`${row.deviceTime}-${index}`} className="vp-playback-step">
              <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>
            </div>
          ))}
          {locationItems.length === 0 ? (
            <Typography.Text type="secondary">当前筛选范围暂无轨迹点，请调整车辆、来源或时间范围。</Typography.Text>
          ) : null}
        </div>
        <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>
      </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>
  );
}
