import { useQuery } from '@tanstack/react-query';
import {
  IconBox, IconChevronLeft, IconChevronRight, IconClose, IconDownload, IconEyeClosed,
  IconEyeOpened, IconList, IconMapPin, IconPause, IconPlay, IconRefresh, IconSearch
} from '@douyinfe/semi-icons';
import { FormEvent, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { api } from '../../api/client';
import type { TrackPlaybackEvent, TrackPlaybackResponse, VehicleRow } from '../../api/types';
import { buildTrackRailSelection, downloadTrackCsv, formatDuration, sampledEventIndex, TRACK_ADDRESS_SETTLE_MS, trackAddressCoordinate, trackPlaybackInterval, type TrackAddressCoordinate } from '../domain/track';
import { TrackMap } from '../map/TrackMap';
import { InlineError } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { QUERY_MEMORY } from '../queryPolicy';
import { monitorReturnFromParams, preserveMonitorReturn } from '../routing/monitorContext';

const speedOptions = [0.5, 1, 2, 4] as const;
type PlaybackSpeed = (typeof speedOptions)[number];
type PanelTab = 'stops' | 'events' | 'overview';
type Draft = { keyword: string; dateFrom: string; dateTo: string; protocol: string };
const EMPTY_INDEXES: readonly number[] = [];
const numberFormatters = new Map<number, Intl.NumberFormat>();
const timeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const dateTimeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false, month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });

function number(value: number, digits = 1) {
  let formatter = numberFormatters.get(digits);
  if (!formatter) {
    formatter = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: digits });
    numberFormatters.set(digits, formatter);
  }
  return formatter.format(Number.isFinite(value) ? value : 0);
}

function direction(value?: number) {
  if (value === undefined || !Number.isFinite(value)) return '方向 —';
  const normalized = ((value % 360) + 360) % 360;
  const names = ['北', '东北', '东', '东南', '南', '西南', '西', '西北'];
  return `${number(normalized, 0)}° ${names[Math.round(normalized / 45) % names.length]}`;
}

function alarm(value?: number) {
  if (value === undefined) return '报警 —';
  return value === 0 ? '无报警' : `报警 0x${Math.trunc(value).toString(16).toUpperCase().padStart(8, '0')}`;
}

function time(value?: string) {
  if (!value) return '—';
  const parsed = new Date(value);
  if (!Number.isNaN(parsed.getTime())) return timeFormatter.format(parsed);
  return value.split(' ').pop()?.slice(0, 8) || '—';
}

function dateTime(value?: string) {
  if (!value) return '—';
  const parsed = new Date(value);
  if (!Number.isNaN(parsed.getTime())) return dateTimeFormatter.format(parsed);
  return value.replace('T', ' ').slice(5, 19);
}

function localDateTime(value: Date) {
  const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000);
  return local.toISOString().slice(0, 16);
}

function trackWindow(daysBack = 0, fullDay = false) {
  const end = new Date();
  end.setDate(end.getDate() - daysBack);
  if (fullDay) end.setHours(23, 59, 59, 999);
  const start = new Date(end);
  start.setHours(0, 0, 0, 0);
  return { dateFrom: localDateTime(start), dateTo: localDateTime(end) };
}

function defaultTrackWindow() {
  return trackWindow();
}

function eventTone(type: string) {
  if (type === 'start') return 'start';
  if (type === 'end' || type === 'braking' || type === 'gap') return 'end';
  if (type === 'acceleration' || type === 'stop') return 'warning';
  return 'info';
}

function VehiclePicker({ value, onChange, onSelect }: { value: string; onChange: (value: string) => void; onSelect: (vehicle: VehicleRow) => void }) {
  const [open, setOpen] = useState(false);
  const [debounced, setDebounced] = useState(value.trim());
  const closeTimerRef = useRef<number>();
  useEffect(() => { const timer = window.setTimeout(() => setDebounced(value.trim()), 220); return () => window.clearTimeout(timer); }, [value]);
  useEffect(() => () => window.clearTimeout(closeTimerRef.current), []);
  const openPicker = () => {
    window.clearTimeout(closeTimerRef.current);
    setOpen(true);
  };
  const closePicker = () => {
    window.clearTimeout(closeTimerRef.current);
    closeTimerRef.current = window.setTimeout(() => setOpen(false), 140);
  };
  const params = useMemo(() => {
    const next = new URLSearchParams({ limit: '10', offset: '0' });
    if (debounced) next.set('keyword', debounced);
    return next;
  }, [debounced]);
  const candidates = useQuery({ queryKey: ['track-vehicle-options', params.toString()], queryFn: ({ signal }) => api.vehicles(params, signal), enabled: open, staleTime: 30_000, gcTime: QUERY_MEMORY.optionGcTime });

  return <div className={`v2-track-vehicle-picker${open ? ' is-open' : ''}`}>
    <IconSearch />
    <input
      aria-label="搜索轨迹车辆" autoComplete="off" placeholder="输入车牌 / VIN / 终端标识"
      value={value} onFocus={openPicker} onBlur={closePicker}
      onChange={(event) => { onChange(event.target.value); setOpen(true); }}
    />
    {value ? <button type="button" aria-label="清空车辆" onMouseDown={(event) => event.preventDefault()} onClick={() => onChange('')}><IconClose /></button> : null}
    {open ? <div className="v2-track-vehicle-options" role="listbox">
      <header><span>车辆候选</span><em>支持车牌或 VIN</em></header>
      {candidates.isFetching ? <p><span className="v2-spinner" />正在搜索车辆</p> : null}
      {!candidates.isFetching && candidates.isError ? <div className="v2-vehicle-option-error" role="alert"><span>{candidates.error instanceof Error ? candidates.error.message : '车辆候选加载失败'}</span><button type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => void candidates.refetch()}>重试</button></div> : null}
      {!candidates.isFetching && (candidates.data?.items ?? []).map((vehicle) => <button
        type="button" role="option" aria-selected={false} key={vehicle.vin}
        onMouseDown={(event) => event.preventDefault()} onClick={() => { onSelect(vehicle); setOpen(false); }}
      ><strong>{vehicle.plate || '未绑定车牌'}</strong><span>{vehicle.vin}</span><em>选择</em></button>)}
      {!candidates.isFetching && !candidates.isError && !(candidates.data?.items.length) ? <p>没有匹配车辆</p> : null}
    </div> : null}
  </div>;
}

function OverviewPanel({ track }: { track: TrackPlaybackResponse }) {
  return <div className="v2-track-overview-panel">
    <section><header><strong>行程概览</strong><span>{track.sampled ? '地图已抽稀' : '完整点集'}</span></header><dl>
      <div><dt>开始时间</dt><dd>{dateTime(track.summary.startTime)}</dd></div>
      <div><dt>结束时间</dt><dd>{dateTime(track.summary.endTime)}</dd></div>
      <div><dt>行驶里程</dt><dd>{number(track.summary.distanceKm)} km</dd></div>
      <div><dt>行驶 / 停车</dt><dd>{formatDuration(track.summary.movingSeconds)} / {formatDuration(track.summary.stoppedSeconds)}</dd></div>
      <div><dt>平均 / 最高速度</dt><dd>{number(track.summary.averageSpeedKmh)} / {number(track.summary.maximumSpeedKmh)} km/h</dd></div>
      <div><dt>停车 / 分段</dt><dd>{track.summary.stopCount} / {track.summary.segmentCount}</dd></div>
    </dl></section>
    <section><header><strong>数据来源</strong><span>{track.coverage.totalPoints.toLocaleString('zh-CN')} 个源点</span></header><div className="v2-track-source-list">{track.sources.map((source) => <article key={source.protocol}><strong>{source.protocol}</strong><span>{source.pointCount.toLocaleString('zh-CN')} 点</span><small>{time(source.startTime)}–{time(source.endTime)}</small></article>)}</div></section>
    <section className={`v2-track-quality-card is-${track.quality.status}`}><header><strong>轨迹质量</strong><span>{track.quality.status === 'good' ? '通过' : '需关注'}</span></header><p>{track.quality.evidence}</p><small>无效 {track.quality.invalidCoordinatePoints} · 重复 {track.quality.duplicatePoints} · 漂移 {track.quality.driftPoints} · 大间隔 {track.quality.largeGapCount}</small></section>
  </div>;
}

const TrackRail = memo(function TrackRail({ draft, track, activeStopIndexes, activeEventIndexes, tab, onDraft, onSubmit, onTab, onSelectIndex, onCollapse }: {
  draft: Draft;
  track?: TrackPlaybackResponse;
  activeStopIndexes: readonly number[];
  activeEventIndexes: readonly number[];
  tab: PanelTab;
  onDraft: (draft: Draft) => void;
  onSubmit: (event: FormEvent) => void;
  onTab: (tab: PanelTab) => void;
  onSelectIndex: (index: number) => void;
  onCollapse: () => void;
}) {
  const choosePreset = (daysBack: number) => onDraft({ ...draft, ...trackWindow(daysBack, daysBack > 0) });
  return <aside className="v2-track-rail">
    <form className="v2-track-query" onSubmit={onSubmit}>
      <header><div><strong>轨迹查询</strong><span>最长支持连续 7 天</span></div><button type="button" aria-label="收起查询面板" onClick={onCollapse}><IconChevronLeft /></button></header>
      <label><span>车辆</span><VehiclePicker value={draft.keyword} onChange={(keyword) => onDraft({ ...draft, keyword })} onSelect={(vehicle) => onDraft({ ...draft, keyword: vehicle.plate || vehicle.vin })} /></label>
      <div className="v2-track-presets"><button type="button" onClick={() => choosePreset(0)}>今天</button><button type="button" onClick={() => choosePreset(1)}>昨天</button><button type="button" onClick={() => { const end = new Date(); const start = new Date(end); start.setDate(start.getDate() - 2); start.setHours(0, 0, 0, 0); onDraft({ ...draft, dateFrom: localDateTime(start), dateTo: localDateTime(end) }); }}>近 3 天</button></div>
      <div className="v2-track-date-grid"><label><span>开始时间</span><input type="datetime-local" value={draft.dateFrom} onChange={(event) => onDraft({ ...draft, dateFrom: event.target.value })} /></label><label><span>结束时间</span><input type="datetime-local" value={draft.dateTo} onChange={(event) => onDraft({ ...draft, dateTo: event.target.value })} /></label></div>
      <label><span>数据来源</span><select value={draft.protocol} onChange={(event) => onDraft({ ...draft, protocol: event.target.value })}><option value="">自动选择最佳来源</option><option value="GB32960">GB32960 · 仪表盘里程</option><option value="JT808">JT808 · GPS 里程</option><option value="YUTONG_MQTT">YUTONG · 仪表盘里程</option></select></label>
      <button className="v2-track-query-button" type="submit" disabled={!draft.keyword.trim()}><IconSearch />查询轨迹</button>
    </form>

    <div className="v2-track-rail-result">
      {track ? <div className="v2-track-rail-vehicle"><span><IconBox /></span><div><strong>{track.plate || track.vin}</strong><small>{track.vin}</small></div><em>{number(track.summary.distanceKm)} km</em></div> : null}
      <nav aria-label="轨迹明细分类"><button type="button" className={tab === 'stops' ? 'is-active' : ''} onClick={() => onTab('stops')}>停留点 <b>{track?.stops.length ?? 0}</b></button><button type="button" className={tab === 'events' ? 'is-active' : ''} onClick={() => onTab('events')}>事件点 <b>{track?.events.length ?? 0}</b></button><button type="button" className={tab === 'overview' ? 'is-active' : ''} onClick={() => onTab('overview')}>概览</button></nav>
      <div className="v2-track-rail-scroll">
        {!track ? <div className="v2-track-rail-empty"><IconMapPin /><strong>选择车辆后查询轨迹</strong><p>停留点、轨迹事件与行程证据会在这里统一呈现。</p></div> : null}
        {track && tab === 'stops' ? <div className="v2-track-stop-list">{track.stops.map((stop, index) => <button type="button" className={activeStopIndexes.includes(index) ? 'is-active' : ''} key={`${stop.startTime}-${index}`} onClick={() => onSelectIndex(stop.sampledIndex)}><i>{index + 1}</i><span><strong>{dateTime(stop.startTime)}</strong><small>停留 {formatDuration(stop.durationSeconds)} · {stop.pointCount} 个点</small></span><em>{time(stop.endTime)}</em></button>)}{!track.stops.length ? <p className="v2-track-list-empty">当前时间窗没有超过 3 分钟的停留点</p> : null}</div> : null}
        {track && tab === 'events' ? <div className="v2-track-event-list">{track.events.map((event, index) => { const sampled = sampledEventIndex(event, track.points.length, track.summary.pointCount); return <button type="button" className={activeEventIndexes.includes(index) ? 'is-active' : ''} key={`${event.type}-${event.time}-${index}`} onClick={() => onSelectIndex(sampled)}><i className={`is-${eventTone(event.type)}`}>{index + 1}</i><span><strong>{event.title}</strong><small>{dateTime(event.time)}</small></span><em>{number(event.speedKmh, 0)} km/h</em></button>; })}</div> : null}
        {track && tab === 'overview' ? <OverviewPanel track={track} /> : null}
      </div>
    </div>
  </aside>;
});

const SegmentRail = memo(function SegmentRail({ track, onSelectIndex }: { track: TrackPlaybackResponse; onSelectIndex: (index: number) => void }) {
  const segments = track.segments.slice(0, 160);
  const total = Math.max(1, segments.reduce((sum, segment) => sum + Math.max(1, segment.durationSeconds), 0));
  return <div className="v2-track-segment-rail" aria-label="轨迹活动分段">{segments.map((segment) => <button
    aria-label={`${segment.title} ${time(segment.startTime)} 至 ${time(segment.endTime)}`}
    className={`is-${segment.type}`} key={`${segment.index}-${segment.startTime}`}
    onClick={() => onSelectIndex(segment.sampledStartIndex)} style={{ flexGrow: Math.max(1, segment.durationSeconds) / total }}
    title={`${segment.title} · ${formatDuration(segment.durationSeconds)} · ${number(segment.distanceKm)} km`}
    type="button"
  />)}</div>;
});

export default function TrackPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const monitorReturn = monitorReturnFromParams(searchParams);
  const fallback = useMemo(defaultTrackWindow, []);
  const routeKey = searchParams.toString();
  const criteria = useMemo<Draft>(() => {
    const params = new URLSearchParams(routeKey);
    return {
      keyword: params.get('vin') || params.get('keyword') || '',
      dateFrom: params.get('dateFrom') || fallback.dateFrom,
      dateTo: params.get('dateTo') || fallback.dateTo,
      protocol: params.get('protocol') || ''
    };
  }, [fallback, routeKey]);
  const [draft, setDraft] = useState(criteria);
  const [activeIndex, setActiveIndex] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [playbackSpeed, setPlaybackSpeed] = useState<PlaybackSpeed>(1);
  const [follow, setFollow] = useState(true);
  const [showStops, setShowStops] = useState(true);
  const [panelTab, setPanelTab] = useState<PanelTab>('stops');
  const [railCollapsed, setRailCollapsed] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 700px)').matches);
  const animationRef = useRef<number>();
  const lastFrameRef = useRef(0);

  const params = useMemo(() => {
    const next = new URLSearchParams({ keyword: criteria.keyword, maxPoints: '1600' });
    if (criteria.dateFrom) next.set('dateFrom', criteria.dateFrom);
    if (criteria.dateTo) next.set('dateTo', criteria.dateTo);
    if (criteria.protocol) next.set('protocol', criteria.protocol);
    return next;
  }, [criteria]);
  const query = useQuery({ queryKey: ['track-playback', params.toString()], enabled: Boolean(criteria.keyword), queryFn: ({ signal }) => api.trackPlayback(params, signal), gcTime: QUERY_MEMORY.highVolumeGcTime });
  const track = query.data;
  const points = track?.points ?? [];
  const boundedIndex = Math.min(activeIndex, Math.max(points.length - 1, 0));
  const current = points[boundedIndex];
  const railSelection = useMemo(() => buildTrackRailSelection(track), [track]);
  const activeStopIndexes = railSelection.stopIndexesByPoint[boundedIndex] ?? EMPTY_INDEXES;
  const activeEventIndexes = railSelection.eventIndexesByPoint[boundedIndex] ?? EMPTY_INDEXES;
  const currentAddressPoint = useMemo(() => trackAddressCoordinate(current?.longitude, current?.latitude), [current?.latitude, current?.longitude]);
  const [addressPoint, setAddressPoint] = useState<TrackAddressCoordinate>();

  useEffect(() => { setDraft(criteria); }, [criteria]);
  useEffect(() => {
    if (playing || !currentAddressPoint) return;
    const timer = window.setTimeout(() => setAddressPoint(currentAddressPoint), TRACK_ADDRESS_SETTLE_MS);
    return () => window.clearTimeout(timer);
  }, [currentAddressPoint?.key, playing]);
  const addressQuery = useQuery({
    queryKey: ['track-address', addressPoint?.key],
    enabled: Boolean(addressPoint), staleTime: 60 * 60 * 1000,
    queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: addressPoint!.longitude.toFixed(4), latitude: addressPoint!.latitude.toFixed(4) }), signal),
    gcTime: QUERY_MEMORY.highVolumeGcTime
  });
  const addressSettling = Boolean(!playing && currentAddressPoint && currentAddressPoint.key !== addressPoint?.key);

  useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); setAddressPoint(undefined); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]);
  useEffect(() => {
    if (!playing || points.length < 2) return;
    lastFrameRef.current = 0;
    const interval = trackPlaybackInterval(playbackSpeed);
    const tick = (timestamp: number) => {
      if (!lastFrameRef.current) lastFrameRef.current = timestamp;
      if (timestamp - lastFrameRef.current >= interval) {
        const steps = Math.max(1, Math.floor((timestamp - lastFrameRef.current) / interval));
        lastFrameRef.current = timestamp;
        setActiveIndex((index) => {
          const next = Math.min(points.length - 1, index + steps);
          if (next >= points.length - 1) setPlaying(false);
          return next;
        });
      }
      animationRef.current = window.requestAnimationFrame(tick);
    };
    animationRef.current = window.requestAnimationFrame(tick);
    return () => { if (animationRef.current) window.cancelAnimationFrame(animationRef.current); };
  }, [playing, playbackSpeed, points.length]);

  const submit = useCallback((event: FormEvent) => {
    event.preventDefault();
    const keyword = draft.keyword.trim();
    if (!keyword) return;
    const next = { ...draft, keyword };
    const url = new URLSearchParams({ keyword });
    if (next.dateFrom) url.set('dateFrom', next.dateFrom);
    if (next.dateTo) url.set('dateTo', next.dateTo);
    if (next.protocol) url.set('protocol', next.protocol);
    setSearchParams(preserveMonitorReturn(url, monitorReturn), { replace: true });
  }, [draft, monitorReturn, setSearchParams]);
  const selectIndex = useCallback((index: number) => { setPlaying(false); setActiveIndex(Math.max(0, Math.min(points.length - 1, index))); }, [points.length]);
  const collapseRail = useCallback(() => setRailCollapsed(true), []);
  const togglePlayback = () => {
    if (points.length < 2) return;
    if (!playing && boundedIndex >= points.length - 1) setActiveIndex(0);
    setFollow(true);
    setPlaying((value) => !value);
  };
  const progress = points.length > 1 ? boundedIndex / (points.length - 1) * 100 : 0;

  return <div className={`v2-track-page${railCollapsed ? ' is-rail-collapsed' : ''}`}>
    <MonitorReturnBar />
    <TrackRail draft={draft} track={track} activeStopIndexes={activeStopIndexes} activeEventIndexes={activeEventIndexes} tab={panelTab} onDraft={setDraft} onSubmit={submit} onTab={setPanelTab} onSelectIndex={selectIndex} onCollapse={collapseRail} />
    <section className="v2-track-stage">
      <TrackMap points={points} stops={track?.stops ?? []} activeIndex={boundedIndex} showStops={showStops} follow={follow} followDurationMs={playing ? Math.max(40, trackPlaybackInterval(playbackSpeed) - 10) : 180} onSelectIndex={selectIndex} onFollowChange={setFollow} />

      <div className="v2-track-stage-tools">
        {railCollapsed ? <button type="button" aria-label="展开查询与明细" onClick={() => setRailCollapsed(false)}><IconList /><span>查询与明细</span></button> : null}
        <button type="button" aria-label={follow ? '关闭车辆跟随' : '开启车辆跟随'} className={follow ? 'is-active' : ''} disabled={!points.length} onClick={() => setFollow((value) => !value)}><IconMapPin /><span>{follow ? '跟随车辆' : '自由浏览'}</span></button>
        <button type="button" aria-label={showStops ? '隐藏停留点' : '显示停留点'} className={showStops ? 'is-active' : ''} disabled={!track?.stops.length} onClick={() => setShowStops((value) => !value)}>{showStops ? <IconEyeOpened /> : <IconEyeClosed />}<span>停留点</span></button>
        <button type="button" aria-label="导出轨迹 CSV" disabled={!track?.points.length} onClick={() => track && downloadTrackCsv(track)}><IconDownload /><span>导出</span></button>
      </div>

      {track?.points.length ? <>
        <div className={`v2-track-coverage-float${track.coverage.complete ? '' : ' is-warning'}`}><i />
          <span><strong>{track.coverage.complete ? '时间窗完整' : '仅展示最新切片'}</strong>{track.coverage.evidence}</span>
          <em>{track.coverage.processedPoints.toLocaleString('zh-CN')} 有效点 → {track.coverage.returnedPoints.toLocaleString('zh-CN')} 地图点</em>
        </div>
        <article className="v2-track-current-card">
          <header><div><strong>{track.plate || track.vin}</strong><span>{dateTime(current?.deviceTime)}</span></div><b>{number(progress, 0)}%</b></header>
          <div><span><small>速度</small><strong>{number(current?.speedKmh ?? 0)}<em> km/h</em></strong></span><span><small>方向</small><strong>{direction(current?.directionDeg)}</strong></span><span><small>SOC</small><strong>{current?.socAvailable ? `${number(current.socPercent)}%` : '—'}</strong></span></div>
          <p title={addressSettling ? undefined : addressQuery.data?.formattedAddress}>{playing ? '播放中，暂停地址解析' : addressSettling ? '位置已变化，等待地址解析…' : addressQuery.isFetching ? '地址解析中…' : addressQuery.data?.formattedAddress || `${current?.longitude.toFixed(6)}, ${current?.latitude.toFixed(6)}`}</p>
        </article>
      </> : <div className="v2-track-empty-state"><IconMapPin /><strong>先选择车辆，再开始轨迹回放</strong><p>地图会显示完整路径、停留点、事件点和播放位置。</p><button type="button" onClick={() => setRailCollapsed(false)}><IconSearch />选择车辆</button></div>}

      {query.isFetching ? <div className="v2-track-loading"><span className="v2-spinner" />正在读取并加工历史轨迹</div> : null}
      {query.isError ? <div className="v2-track-error"><InlineError message={query.error instanceof Error ? query.error.message : '轨迹查询失败'} onRetry={() => query.refetch()} /></div> : null}

      <footer className="v2-track-playback-dock">
        <div className="v2-track-dock-summary"><strong>{track ? `${number(track.summary.distanceKm)} km` : '等待查询'}</strong><span>{track ? `${formatDuration(track.summary.durationSeconds)} · ${track.summary.stopCount} 次停留` : '查询后可播放完整轨迹'}</span></div>
        <div className="v2-track-dock-progress">
          {track ? <SegmentRail track={track} onSelectIndex={selectIndex} /> : <div className="v2-track-segment-placeholder" />}
          <input aria-label="轨迹播放进度" type="range" min="0" max={Math.max(0, points.length - 1)} value={boundedIndex} onChange={(event) => selectIndex(Number(event.target.value))} disabled={!points.length} style={{ '--track-progress': `${progress}%` } as React.CSSProperties} />
          <div><time>{time(current?.deviceTime)}</time><span>数据点 {points.length ? boundedIndex + 1 : 0} / {points.length}</span><time>{time(track?.summary.endTime)}</time></div>
        </div>
        <div className="v2-track-dock-controls">
          <button type="button" aria-label="上一个轨迹点" onClick={() => selectIndex(boundedIndex - 1)} disabled={!boundedIndex}><IconChevronLeft /></button>
          <button type="button" className="is-primary" aria-label={playing ? '暂停轨迹播放' : '开始轨迹播放'} onClick={togglePlayback} disabled={points.length < 2}>{playing ? <IconPause /> : <IconPlay />}</button>
          <button type="button" aria-label="下一个轨迹点" onClick={() => selectIndex(boundedIndex + 1)} disabled={!points.length || boundedIndex >= points.length - 1}><IconChevronRight /></button>
          <label><span>速度</span><select value={playbackSpeed} onChange={(event) => setPlaybackSpeed(Number(event.target.value) as PlaybackSpeed)}>{speedOptions.map((speed) => <option value={speed} key={speed}>{speed}×</option>)}</select></label>
          <button type="button" aria-label="回到起点" onClick={() => selectIndex(0)} disabled={!boundedIndex}><IconRefresh /></button>
        </div>
        <div className="v2-track-dock-metrics"><span><small>总里程</small><strong>{number(current?.totalMileageKm ?? 0)} km</strong></span><span><small>数据来源</small><strong>{current?.protocol || '—'}</strong></span><span><small>车辆状态</small><strong>{alarm(current?.alarmFlag)}</strong></span></div>
      </footer>
    </section>
  </div>;
}
