import { useMemo, useEffect, useRef, useCallback, useState } from 'react'
import Map, { Source, Layer } from 'react-map-gl'
import type { MapRef } from 'react-map-gl'
import type { Map as MapboxMap } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { useReplaySituation } from '@/hooks/useReplaySituation'
import { usePlaybackStore } from '@/store/playbackStore'
import { config } from '@/config'
import { EXTENDED_WAR_ZONES, KURDISH_FRONT_GEOJSON } from '@/data/extendedWarData'
import { useWarMapData } from '@/hooks/useWarMapData'
import { createTacticalPincerAtProgress } from '@/utils/tacticalPincerArrow'

const MAPBOX_TOKEN = config.mapboxAccessToken || ''

// 相关区域 bbox：伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat)，覆盖红蓝区域
const THEATER_BBOX = [22, 11, 64, 41] as const
/** 移动端/小屏时 fitBounds 使区域完整显示 */
const THEATER_BOUNDS: [[number, number], [number, number]] = [
  [THEATER_BBOX[0], THEATER_BBOX[1]],
  [THEATER_BBOX[2], THEATER_BBOX[3]],
]
const THEATER_CENTER = {
  longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2,
  latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2,
}
const DEFAULT_VIEW = { ...THEATER_CENTER, zoom: 4.2 }

const COUNTRIES_GEOJSON =
  'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'

// 胡塞武装失控区 [lng, lat] 闭环
const HOUTHI_POLYGON: [number, number][] = [
  [42.7, 15.8],
  [43.3, 16.5],
  [45.1, 17.2],
  [45.8, 15.1],
  [44.2, 13.5],
  [42.7, 15.8],
]

const IRAN_ADMIN = 'Iran'
const ALLIES_ADMIN = [
  'Qatar',
  'Bahrain',
  'Kuwait',
  'United Arab Emirates',
  'Saudi Arabia',
  'Iraq',
  'Syria',
  'Jordan',
  'Turkey',
  'Israel',
  'Oman',
  'Egypt',
  'Djibouti',
]

// 伊朗攻击源 德黑兰 [lng, lat]（若后端 map_strike_source 有 iran/tehran 则可由 API 覆盖）
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]

/** 攻击动画时间衰减：N 天内按天衰减（脉冲缩小、频次降低），超出仅保留减弱呼吸效果；天数可在编辑面板配置 */
const MS_PER_DAY = 24 * 60 * 60 * 1000
/** 手动添加的打击源：始终参与实时渲染列表，用 strikeCutoffDays 做衰减；仅在图例选中该项时不衰减 */
const MANUAL_STRIKE_SOURCE_IDS = new Set(['virginia_srilanka', 'tomahawk_arabian', 'arabian_sea_sub_torpedo'])
function isWithinAnimationWindow(
  iso: string | null | undefined,
  referenceTime: string,
  cutoffDays: number
): boolean {
  if (!iso) return true
  const ref = new Date(referenceTime).getTime()
  const t = new Date(iso).getTime()
  const days = (ref - t) / MS_PER_DAY
  return days >= 0 && days <= cutoffDays
}
/** 衰减系数 0..1：天数越久越小，用于缩小脉冲范围、降低攻击频次 */
function getDecayFactor(iso: string | null | undefined, referenceTime: string, cutoffDays: number): number {
  if (!iso) return 1
  const ref = new Date(referenceTime).getTime()
  const t = new Date(iso).getTime()
  const days = (ref - t) / MS_PER_DAY
  if (days < 0 || days > cutoffDays) return 0
  return Math.max(0, 1 - days / cutoffDays)
}

/** 从 GeoJSON 多边形/多边形的 coordinates 中提取所有 [lng, lat] 并计算 bbox */
function bboxFromCoords(coords: number[][][] | number[][][][]): [number, number, number, number] {
  const points: [number, number][] = []
  const push = (c: number[]) => {
    if (c.length >= 2) points.push([c[0], c[1]])
  }
  const flatten = (arr: number[][] | number[][][] | number[][][][]) => {
    for (const item of arr) {
      if (typeof item[0] === 'number') push(item as number[])
      else flatten(item as number[][][] | number[][][][])
    }
  }
  flatten(coords)
  if (points.length === 0) return [0, 0, 1, 1]
  const lngs = points.map((p) => p[0])
  const lats = points.map((p) => p[1])
  return [Math.min(...lngs), Math.min(...lats), Math.max(...lngs), Math.max(...lats)]
}

// API 未返回 mapData 时的静态 fallback，保证美/以打击线与动画不消失（与 server/seed.js 一致）
const FALLBACK_STRIKE_SOURCES: { id: string; name: string; lng: number; lat: number }[] = [
  { id: 'israel', name: '以色列', lng: 34.78, lat: 32.08 },
  { id: 'lincoln', name: '林肯号航母', lng: 58.4215, lat: 24.1568 },
  { id: 'ford', name: '福特号航母', lng: 24.1002, lat: 35.7397 },
  { id: 'virginia_srilanka', name: '洛杉矶级攻击核潜艇(斯里兰卡外海)', lng: 78.5, lat: 5.2 },
  { id: 'tomahawk_arabian', name: '阿拉伯海潜艇(战斧)', lng: 59.2, lat: 23.0 },
  { id: 'arabian_sea_sub_torpedo', name: '阿拉伯海潜艇(鱼雷)', lng: 59.2, lat: 23.0 },
]
const FALLBACK_STRIKE_LINES: { sourceId: string; targets: { lng: number; lat: number; name?: string; struck_at?: string | null }[] }[] = [
  {
    sourceId: 'israel',
    targets: [
      { lng: 50.88, lat: 34.64, name: '库姆' },
      { lng: 50.876409, lat: 34.625448, name: '伊朗专家会议秘书处' },
      { lng: 51.916, lat: 33.666, name: '纳坦兹' },
      { lng: 51.002, lat: 35.808, name: '卡拉季无人机厂' },
      { lng: 35.5, lat: 33.86, name: '贝鲁特南郊指挥所' },
      { lng: 35.32, lat: 33.34, name: '利塔尼弹药库' },
      { lng: 36.2, lat: 34.01, name: '巴勒贝克后勤枢纽' },
      { lng: 35.19, lat: 33.27, name: '提尔海岸阵地' },
      { lng: 36.38, lat: 34.39, name: '赫尔梅勒无人机阵地' },
    ],
  },
  {
    sourceId: 'lincoln',
    targets: [
      { lng: 56.27, lat: 27.18, name: '阿巴斯港海军司令部' },
      { lng: 57.08, lat: 27.13, name: '米纳布' },
      { lng: 56.5, lat: 27.0, name: '霍尔木兹岸防阵地' },
      { lng: 50.838, lat: 28.968, name: '布什尔雷达站' },
      { lng: 51.667, lat: 32.654, name: '伊斯法罕核设施' },
    ],
  },
  {
    sourceId: 'ford',
    targets: [
      { lng: 51.42, lat: 35.69, name: '哈梅内伊官邸' },
      { lng: 51.41, lat: 35.72, name: '总统府/情报部' },
      { lng: 51.15, lat: 35.69, name: '梅赫拉巴德机场' },
      { lng: 46.29, lat: 38.08, name: '大不里士空军基地' },
      { lng: 47.076, lat: 34.314, name: '克尔曼沙赫导弹掩体' },
      { lng: 46.42, lat: 33.64, name: '伊拉姆导弹阵地' },
      { lng: 48.35, lat: 33.48, name: '霍拉马巴德储备库' },
    ],
  },
  {
    sourceId: 'virginia_srilanka',
    targets: [
      { lng: 79.8, lat: 6.5, name: '德纳号轻型护卫舰', struck_at: '2026-02-28T08:00:00.000Z' },
    ],
  },
  {
    sourceId: 'tomahawk_arabian',
    targets: [
      { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' },
    ],
  },
  {
    sourceId: 'arabian_sea_sub_torpedo',
    targets: [
      { lng: 56.26, lat: 27.18, name: '沙希德·鲁德基号/无人机航母', struck_at: '2026-02-28T03:00:00.000Z' },
    ],
  },
]

/** 二次贝塞尔曲线路径，更平滑的弧线 height 控制弧高 */
function parabolaPath(
  start: [number, number],
  end: [number, number],
  height = 3
): [number, number][] {
  const ctrl: [number, number] = [
    (start[0] + end[0]) / 2,
    (start[1] + end[1]) / 2 + height,
  ]
  // 生成多段点使曲线更平滑
  const pts: [number, number][] = [start]
  for (let i = 1; i < 12; i++) {
    const s = i / 12
    const t = 1 - s
    const x = t * t * start[0] + 2 * t * s * ctrl[0] + s * s * end[0]
    const y = t * t * start[1] + 2 * t * s * ctrl[1] + s * s * end[1]
    pts.push([x, y])
  }
  pts.push(end)
  return pts
}

/** 沿路径插值，t ∈ [0,1]，支持多点路径 */
function interpolateOnPath(path: [number, number][], t: number): [number, number] {
  if (t <= 0) return path[0]
  if (t >= 1) return path[path.length - 1]
  const n = path.length - 1
  const seg = Math.min(Math.floor(t * n), n - 1)
  const u = (t * n) - seg
  const a = path[seg]
  const b = path[seg + 1]
  return [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
}

type BaseStatus = 'operational' | 'damaged' | 'attacked'

interface KeyLoc {
  name: string
  lat: number
  lng: number
  type?: string
  status?: BaseStatus
  damage_level?: number
}

function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
  return {
    type: 'Feature' as const,
    properties: {
      side,
      name: loc.name,
      status: status ?? (loc as KeyLoc & { status?: BaseStatus }).status ?? 'operational',
    },
    geometry: {
      type: 'Point' as const,
      coordinates: [loc.lng, loc.lat] as [number, number],
    },
  }
}

const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间

/** 移动端/小屏降低动画更新频率以减轻卡顿；返回最小间隔 ms */
function getAnimIntervalMs(): number {
  try {
    if (typeof window === 'undefined') return 33
    const reducedMotion =
      window.matchMedia('(prefers-reduced-motion: reduce)').matches
    if (reducedMotion) return 100 // 约 10fps，兼顾可访问性
    return window.innerWidth <= 768 ? 50 : 33 // 移动端约 20fps，桌面约 30fps
  } catch {
    return 33
  }
}

export function WarMap() {
  const mapRef = useRef<MapRef>(null)
  const containerRef = useRef<HTMLDivElement>(null)
  const animRef = useRef<number>(0)
  const pincerAnimRef = useRef<{ lastProgressStep?: number }>({ lastProgressStep: -1 })
  const startRef = useRef<number>(0)
  const lastAnimUpdateRef = useRef<number>(0)
  const attackPathsRef = useRef<[number, number][][]>([])
  const lincolnPathsRef = useRef<[number, number][][]>([])
  const fordPathsRef = useRef<[number, number][][]>([])
  const israelPathsRef = useRef<[number, number][][]>([])
  const virginiaPathsRef = useRef<[number, number][][]>([])
  const tomahawkPathsRef = useRef<[number, number][][]>([])
  const arabianSubTorpedoPathsRef = useRef<[number, number][][]>([])
  const hezbollahPathsRef = useRef<[number, number][][]>([])
  const hormuzPathsRef = useRef<[number, number][][]>([])
  const [legendOpen, setLegendOpen] = useState(true)
  /** 图例点击筛选：打击类型 或 势力/区域；null = 显示全部；与「林肯打击」同逻辑：只显示该项 + 飞镜头 + 完整动画 */
  type LegendFilterKey =
    | 'lincoln'
    | 'ford'
    | 'israel'
    | 'virginia_srilanka'
    | 'tomahawk_arabian'
    | 'hezbollah'
    | 'iranToUs'
    | 'iran'
    | 'hormuz'
    | 'hezbollahZone'
    | 'kurdish'
  const [strikeLegendFilter, setStrikeLegendFilter] = useState<LegendFilterKey | null>(null)
  const strikeLegendFilterRef = useRef<LegendFilterKey | null>(null)
  strikeLegendFilterRef.current = strikeLegendFilter
  const getBoundsForFilterRef = useRef<(k: LegendFilterKey | null) => [number, number, number, number] | null>(() => null)
  const toggleStrikeFilter = (key: LegendFilterKey | null) => {
    const next = strikeLegendFilter === key ? null : key
    strikeLegendFilterRef.current = next
    // 点击时同步飞镜头；移动端减小 padding、缩短 duration，使小屏更顺滑
    if (next != null) {
      const bounds = getBoundsForFilterRef.current(next)
      const map = mapRef.current?.getMap()
      if (bounds && map) {
        const isMobile = typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches
        map.fitBounds(
          [
            [bounds[0], bounds[1]],
            [bounds[2], bounds[3]],
          ],
          {
            padding: isMobile ? 40 : 80,
            maxZoom: isMobile ? 7.5 : 8,
            duration: isMobile ? 400 : 500,
            easing: (t) => 1 - Math.pow(1 - t, 3), // easeOutCubic
          }
        )
      }
    }
    setStrikeLegendFilter(next)
  }
  const situation = useReplaySituation()
  const { isReplayMode, playbackTime } = usePlaybackStore()
  const { usForces, iranForces, conflictEvents = [] } = situation
  /** 时间衰减基准：回放模式用回放时刻；实时模式用「当前数据中最新 struck_at/attacked_at」，无事件时再用 lastUpdated/now。
   * 这样爬虫推送后 lastUpdated 跳到「当前」时，窗口仍以数据中最新事件为右端，不会前移导致已有攻击路线被排除（动画消失）。 */
  const referenceTime = useMemo(() => {
    if (isReplayMode) return playbackTime
    let maxTs = 0
    for (const line of situation.mapData?.strikeLines ?? []) {
      for (const t of line.targets ?? []) {
        if (t.struck_at) {
          const ts = new Date(t.struck_at).getTime()
          if (ts > maxTs) maxTs = ts
        }
      }
    }
    for (const loc of [...(usForces.keyLocations ?? []), ...(iranForces.keyLocations ?? [])]) {
      if (loc.attacked_at) {
        const ts = new Date(loc.attacked_at).getTime()
        if (ts > maxTs) maxTs = ts
      }
    }
    if (maxTs > 0) return new Date(maxTs).toISOString()
    return situation.lastUpdated || new Date().toISOString()
  }, [
    isReplayMode,
    playbackTime,
    situation.lastUpdated,
    situation.mapData?.strikeLines,
    usForces.keyLocations,
    iranForces.keyLocations,
  ])

  const usLocs = (usForces.keyLocations || []) as KeyLoc[]
  const irLocs = (iranForces.keyLocations || []) as KeyLoc[]

  // 移动端默认收起右侧图例，避免遮挡；桌面端默认展开
  useEffect(() => {
    if (typeof window === 'undefined') return
    const isMobile = window.matchMedia('(max-width: 640px)').matches
    if (isMobile) setLegendOpen(false)
  }, [])

  /** 移动端打击面板收纳：默认展开，可手动收纳以腾出地图 */
  const [strikePanelOpen, setStrikePanelOpen] = useState(true)

  const { usNaval, usBaseOp, usBaseDamaged, usBaseAttacked, labelsGeoJson } = useMemo(() => {
    const naval: GeoJSON.Feature<GeoJSON.Point>[] = []
    const op: GeoJSON.Feature<GeoJSON.Point>[] = []
    const damaged: GeoJSON.Feature<GeoJSON.Point>[] = []
    const attacked: GeoJSON.Feature<GeoJSON.Point>[] = []
    const labels: GeoJSON.Feature<GeoJSON.Point>[] = []

    for (const loc of usLocs as KeyLoc[]) {
      const f = toFeature(loc, 'us')
      labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
      if (loc.type === 'Base') {
        const s = (loc.status ?? 'operational') as BaseStatus
        if (s === 'attacked') attacked.push(f)
        else if (s === 'damaged') damaged.push(f)
        else op.push(f)
      } else {
        naval.push(f)
      }
    }

    for (const loc of irLocs) {
      const f = toFeature(loc, 'iran')
      labels.push({ ...f, properties: { ...f.properties, name: loc.name } })
    }

    return {
      usNaval: { type: 'FeatureCollection' as const, features: naval },
      usBaseOp: { type: 'FeatureCollection' as const, features: op },
      usBaseDamaged: { type: 'FeatureCollection' as const, features: damaged },
      usBaseAttacked: { type: 'FeatureCollection' as const, features: attacked },
      labelsGeoJson: { type: 'FeatureCollection' as const, features: labels },
    }
  }, [usForces.keyLocations, iranForces.keyLocations])

  const mapData = situation.mapData
  /** 打击源：API 有则用，并合并手动场景源（斯里兰卡潜艇、阿巴斯港战斧+潜艇鱼雷），缺则从 fallback 补全 */
  const strikeSources = useMemo(() => {
    const fromApi = mapData?.strikeSources?.length > 0 ? mapData.strikeSources : FALLBACK_STRIKE_SOURCES
    const ids = new Set((fromApi as { id: string }[]).map((s) => s.id))
    const toAdd = FALLBACK_STRIKE_SOURCES.filter((s) => !ids.has(s.id))
    return toAdd.length > 0 ? [...fromApi, ...toAdd] : fromApi
  }, [mapData?.strikeSources])
  /** 打击线：API 有则用 API，并始终合并斯里兰卡/阿巴斯港(战斧+潜艇) fallback，保证场景必有数据可渲染 */
  const strikeLines = useMemo(() => {
    const fromApi = mapData?.strikeLines?.length > 0 ? mapData.strikeLines : FALLBACK_STRIKE_LINES
    const hasVirginia = fromApi.some((l) => l.sourceId === 'virginia_srilanka')
    const hasTomahawk = fromApi.some((l) => l.sourceId === 'tomahawk_arabian')
    const hasArabianSubTorpedo = fromApi.some((l) => l.sourceId === 'arabian_sea_sub_torpedo')
    const extra = [
      ...(hasVirginia ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'virginia_srilanka')),
      ...(hasTomahawk ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'tomahawk_arabian')),
      ...(hasArabianSubTorpedo ? [] : FALLBACK_STRIKE_LINES.filter((l) => l.sourceId === 'arabian_sea_sub_torpedo')),
    ]
    return extra.length > 0 ? [...fromApi, ...extra] : fromApi
  }, [mapData?.strikeLines])

  /** 伊朗→美军基地：仅用 DB 数据，N 天内显示飞行动画 */
  const strikeCutoffDays = situation.animationConfig?.strikeCutoffDays ?? 5
  /** 实时模式：渲染最近 5 天内的进展（主视角）；全部=不限时长 */
  const REALTIME_CUTOFF_DAYS = 5
  const UNLIMITED_CUTOFF_DAYS = 365
  const [isRealtimeView, setIsRealtimeView] = useState(true)
  const isRealtimeViewRef = useRef(true)
  isRealtimeViewRef.current = isRealtimeView
  /** 未选中单项时：全部=不限时长(365天)，实时=5 天；选中某项时用 strikeCutoffDays（该单项完整动画） */
  const effectiveCutoffDays =
    strikeLegendFilter === null
      ? (isRealtimeView ? REALTIME_CUTOFF_DAYS : UNLIMITED_CUTOFF_DAYS)
      : strikeCutoffDays

  const attackPaths = useMemo(() => {
    const attacked = (usForces.keyLocations || []).filter(
      (loc): loc is typeof loc & { attacked_at: string } =>
        loc.status === 'attacked' &&
        !!loc.attacked_at &&
        isWithinAnimationWindow(loc.attacked_at, referenceTime, effectiveCutoffDays)
    )
    return attacked.map((loc) => parabolaPath(TEHRAN_SOURCE, [loc.lng, loc.lat]))
  }, [usForces.keyLocations, referenceTime, effectiveCutoffDays])
  /** 图例选中伊朗→美国时用：全部遭袭基地路径（不按时间窗口过滤） */
  const attackPathsAll = useMemo(() => {
    const attacked = (usForces.keyLocations || []).filter(
      (loc): loc is typeof loc & { lng: number; lat: number } =>
        loc.status === 'attacked' && typeof loc.lng === 'number' && typeof loc.lat === 'number'
    )
    return attacked.map((loc) => parabolaPath(TEHRAN_SOURCE, [loc.lng, loc.lat]))
  }, [usForces.keyLocations])

  const sourceCoords = useMemo(() => {
    const m: Record<string, [number, number]> = {}
    strikeSources.forEach((s) => { m[s.id] = [s.lng, s.lat] })
    return m
  }, [strikeSources])

  /** 盟军打击线：仅用 effectiveCutoffDays 内目标显示飞行动画（全部=不限时长，实时=5 天） */
  const filterTargetsByAnimationWindow = useMemo(
    () => (targets: { lng: number; lat: number; struck_at?: string | null }[]) =>
      targets.filter((t) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, effectiveCutoffDays)),
    [referenceTime, effectiveCutoffDays]
  )

  const lincolnPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'lincoln')
    const coords = sourceCoords.lincoln
    if (!coords || !line) return []
    return filterTargetsByAnimationWindow(line.targets).map((t) =>
      parabolaPath(coords, [t.lng, t.lat])
    )
  }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
  const fordPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'ford')
    const coords = sourceCoords.ford
    if (!coords || !line) return []
    return filterTargetsByAnimationWindow(line.targets).map((t) =>
      parabolaPath(coords, [t.lng, t.lat])
    )
  }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
  const israelPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'israel')
    const coords = sourceCoords.israel
    if (!coords || !line) return []
    return filterTargetsByAnimationWindow(line.targets).map((t) =>
      parabolaPath(coords, [t.lng, t.lat])
    )
  }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])

  /** 图例选中时用：该打击的全部路径（不按时间窗口过滤），直接渲染完整结果 */
  const lincolnPathsAll = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'lincoln')
    const coords = sourceCoords.lincoln
    if (!coords || !line) return []
    return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
  }, [strikeLines, sourceCoords])
  const fordPathsAll = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'ford')
    const coords = sourceCoords.ford
    if (!coords || !line) return []
    return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
  }, [strikeLines, sourceCoords])
  const israelPathsAll = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'israel')
    const coords = sourceCoords.israel
    if (!coords || !line) return []
    return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
  }, [strikeLines, sourceCoords])

  /** 场景 A：斯里兰卡深海伏击 — 鱼雷轨迹为直线（潜艇 [78.5,5.2] → 驱逐舰 [79.8,6.5]），不采用抛物线 */
  const virginiaSrilankaPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka')
    const coords = sourceCoords.virginia_srilanka
    if (!coords || !line) return []
    return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][])
  }, [strikeLines, sourceCoords])
  /** 场景 B：阿拉伯海战斧 → 阿巴斯港，抛物线弹道 */
  const tomahawkArabianPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian')
    const coords = sourceCoords.tomahawk_arabian
    if (!coords || !line) return []
    return line.targets.map((t) => parabolaPath(coords, [t.lng, t.lat]))
  }, [strikeLines, sourceCoords])
  /** 上述两场景在时间窗口内的路径（与林肯/福特/以色列一致，用于实时模式） */
  const effectiveVirginiaSrilankaPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'virginia_srilanka')
    const coords = sourceCoords.virginia_srilanka
    if (!coords || !line) return []
    return filterTargetsByAnimationWindow(line.targets).map((t) => [coords, [t.lng, t.lat]] as [number, number][])
  }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
  const effectiveTomahawkArabianPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'tomahawk_arabian')
    const coords = sourceCoords.tomahawk_arabian
    if (!coords || !line) return []
    return filterTargetsByAnimationWindow(line.targets).map((t) => parabolaPath(coords, [t.lng, t.lat]))
  }, [strikeLines, sourceCoords, filterTargetsByAnimationWindow])
  /** 场景 B：阿巴斯港 — 同艇潜射/鱼雷轨迹（直线，与战斧同时） */
  const arabianSubTorpedoPaths = useMemo(() => {
    const line = strikeLines.find((l) => l.sourceId === 'arabian_sea_sub_torpedo')
    const coords = sourceCoords.arabian_sea_sub_torpedo
    if (!coords || !line) return []
    return line.targets.map((t) => [coords, [t.lng, t.lat]] as [number, number][])
  }, [strikeLines, sourceCoords])

  /** 黎巴嫩→以色列：攻击源为黎巴嫩境内多处（提尔、西顿、巴勒贝克等），目标为以色列北部 */
  const hezbollahPaths = useMemo(() => {
    const sources = EXTENDED_WAR_ZONES.lebanonStrikeSources
    const targets = EXTENDED_WAR_ZONES.activeAttacks
    return targets.map((t, i) =>
      parabolaPath(sources[i % sources.length], t.coords, 3)
    )
  }, [])

  /** 霍尔木兹海峡被打击目标点位；飞行动画由伊朗多处→该区域 */
  const hormuzTargetPoints = useMemo(
    () =>
      [
        [55.7, 25.6],
        [56.0, 26.0],
        [56.4, 26.4],
      ] as [number, number][],
    []
  )
  /** 伊朗多处→霍尔木兹：德黑兰、克尔曼沙赫、库姆 攻击海峡目标（无日期按 decay=1 显示） */
  const hormuzPaths = useMemo(() => {
    const sources: [number, number][] = [
      TEHRAN_SOURCE,
      [47.16, 34.35],
      [50.88, 34.64],
    ]
    return hormuzTargetPoints.map((target, idx) =>
      parabolaPath(sources[idx % sources.length], target, 3)
    )
  }, [hormuzTargetPoints])

  const warMapData = useWarMapData()

  /** 当前参与动画的目标的最小衰减系数；手动打击(virginia/tomahawk)用 strikeCutoffDays 参与衰减，其余用 effectiveCutoffDays */
  const animationDecayFactor = useMemo(() => {
    const decays: number[] = []
    ;(usForces.keyLocations || [])
      .filter((l) => l.status === 'attacked' && l.attacked_at && isWithinAnimationWindow(l.attacked_at, referenceTime, effectiveCutoffDays))
      .forEach((l) => decays.push(getDecayFactor(l.attacked_at ?? null, referenceTime, effectiveCutoffDays)))
    for (const line of strikeLines) {
      const cutoffDays = MANUAL_STRIKE_SOURCE_IDS.has(line.sourceId) ? strikeCutoffDays : effectiveCutoffDays
      const inWindow = (t: { struck_at?: string | null }) => isWithinAnimationWindow(t.struck_at ?? null, referenceTime, cutoffDays)
      for (const t of line.targets) {
        if (!inWindow(t)) continue
        decays.push(getDecayFactor((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, cutoffDays))
      }
    }
    if (hormuzPaths.length > 0) decays.push(1)
    if (hezbollahPaths.length > 0) decays.push(1)
    return decays.length > 0 ? Math.min(...decays) : 1
  }, [usForces.keyLocations, strikeLines, referenceTime, effectiveCutoffDays, strikeCutoffDays, hormuzPaths.length, hezbollahPaths.length])

  const animationDecayRef = useRef(1)
  animationDecayRef.current = animationDecayFactor

  /** 图例筛选后实际显示的路径。全部/单项选中=全部路径+完整动画；实时=时间窗口内路径+衰减 */
  const effectiveLincolnPaths =
    strikeLegendFilter === 'lincoln'
      ? lincolnPathsAll
      : strikeLegendFilter === null
        ? (isRealtimeView ? lincolnPaths : lincolnPathsAll)
        : []
  const effectiveFordPaths =
    strikeLegendFilter === 'ford'
      ? fordPathsAll
      : strikeLegendFilter === null
        ? (isRealtimeView ? fordPaths : fordPathsAll)
        : []
  const effectiveIsraelPaths =
    strikeLegendFilter === 'israel'
      ? israelPathsAll
      : strikeLegendFilter === null
        ? (isRealtimeView ? israelPaths : israelPathsAll)
        : []
  const effectiveHezbollahPaths =
    strikeLegendFilter === 'hezbollah' || strikeLegendFilter === 'hezbollahZone'
      ? hezbollahPaths
      : strikeLegendFilter === null
        ? hezbollahPaths
        : []
  const effectiveAttackPaths =
    strikeLegendFilter === 'iranToUs' || strikeLegendFilter === 'iran'
      ? attackPathsAll
      : strikeLegendFilter === null
        ? (isRealtimeView ? attackPaths : attackPathsAll)
        : []
  const effectiveHormuzPaths =
    strikeLegendFilter === 'hormuz' || strikeLegendFilter === 'iran' ? hormuzPaths : strikeLegendFilter === null ? hormuzPaths : []

  /** 斯里兰卡伏击 / 阿巴斯港突袭（手动打击）：实时模式下始终进入渲染列表并做衰减；图例选中时完整动画不衰减 */
  const effectiveVirginiaSrilankaPathsDisplay =
    strikeLegendFilter === 'virginia_srilanka'
      ? virginiaSrilankaPaths
      : strikeLegendFilter === null
        ? virginiaSrilankaPaths
        : []
  const effectiveTomahawkArabianPathsDisplay =
    strikeLegendFilter === 'tomahawk_arabian'
      ? tomahawkArabianPaths
      : strikeLegendFilter === null
        ? tomahawkArabianPaths
        : []
  /** 阿巴斯港潜艇直线（与战斧同时）：与 tomahawk_arabian 同显 */
  const effectiveArabianSubTorpedoPathsDisplay =
    strikeLegendFilter === 'tomahawk_arabian' || strikeLegendFilter === null
      ? arabianSubTorpedoPaths
      : []

  lincolnPathsRef.current = effectiveLincolnPaths
  fordPathsRef.current = effectiveFordPaths
  israelPathsRef.current = effectiveIsraelPaths
  virginiaPathsRef.current = effectiveVirginiaSrilankaPathsDisplay
  tomahawkPathsRef.current = effectiveTomahawkArabianPathsDisplay
  arabianSubTorpedoPathsRef.current = effectiveArabianSubTorpedoPathsDisplay
  hezbollahPathsRef.current = effectiveHezbollahPaths
  hormuzPathsRef.current = effectiveHormuzPaths
  attackPathsRef.current = effectiveAttackPaths

  /** 伊朗大致 bbox（图例选中伊朗时飞镜头用） */
  const IRAN_BBOX: [number, number, number, number] = [44, 25, 63, 40]
  const pad = 0.5

  /** 根据图例 key 计算 bbox（点击时同步调用，避免等 useEffect 造成的延迟） */
  const getBoundsForFilter = useCallback(
    (key: LegendFilterKey | null): [number, number, number, number] | null => {
      if (key == null) return null
      const points: [number, number][] = []
      const push = (p: [number, number]) => { points.push(p) }
      const flattenPaths = (paths: [number, number][][]) =>
        paths.forEach((path) => path.forEach((c) => push(c)))
      switch (key) {
        case 'lincoln':
          if (sourceCoords.lincoln) push(sourceCoords.lincoln)
          flattenPaths(lincolnPathsAll)
          break
        case 'ford':
          if (sourceCoords.ford) push(sourceCoords.ford)
          flattenPaths(fordPathsAll)
          break
        case 'israel':
          if (sourceCoords.israel) push(sourceCoords.israel)
          flattenPaths(israelPathsAll)
          break
        case 'virginia_srilanka':
          if (sourceCoords.virginia_srilanka) push(sourceCoords.virginia_srilanka)
          flattenPaths(virginiaSrilankaPaths)
          break
        case 'tomahawk_arabian': {
          const src = sourceCoords.tomahawk_arabian ?? sourceCoords.arabian_sea_sub_torpedo
          if (src) push(src)
          flattenPaths(tomahawkArabianPaths)
          flattenPaths(arabianSubTorpedoPaths)
          break
        }
        case 'hezbollah':
          flattenPaths(hezbollahPaths)
          break
        case 'hezbollahZone': {
          const [minLng, minLat, maxLng, maxLat] = bboxFromCoords(
            EXTENDED_WAR_ZONES.hezbollahZone.geometry.coordinates
          )
          return [minLng - pad, minLat - pad, maxLng + pad, maxLat + pad]
        }
        case 'iranToUs':
          push(TEHRAN_SOURCE)
          flattenPaths(attackPathsAll)
          break
        case 'iran': {
          const [minLng, minLat, maxLng, maxLat] = IRAN_BBOX
          return [minLng - pad, minLat - pad, maxLng + pad, maxLat + pad]
        }
        case 'hormuz': {
          const [minLng, minLat, maxLng, maxLat] = bboxFromCoords(
            EXTENDED_WAR_ZONES.hormuzCombatZone.geometry.coordinates
          )
          return [minLng - pad, minLat - pad, maxLng + pad, maxLat + pad]
        }
        case 'kurdish': {
          const feat = KURDISH_FRONT_GEOJSON.features[0]
          if (feat?.geometry?.type === 'MultiPolygon') {
            const [minLng, minLat, maxLng, maxLat] = bboxFromCoords(feat.geometry.coordinates)
            return [minLng - pad, minLat - pad, maxLng + pad, maxLat + pad]
          }
          const [cx, cy] = EXTENDED_WAR_ZONES.kurdishLabelCenter
          return [cx - 4, cy - 4, cx + 4, cy + 4]
        }
        default:
          return null
      }
      if (points.length === 0) return null
      const lngs = points.map((p) => p[0])
      const lats = points.map((p) => p[1])
      return [
        Math.min(...lngs) - pad,
        Math.min(...lats) - pad,
        Math.max(...lngs) + pad,
        Math.max(...lats) + pad,
      ]
    },
    [
      sourceCoords,
      lincolnPathsAll,
      fordPathsAll,
      israelPathsAll,
      virginiaSrilankaPaths,
      tomahawkArabianPaths,
      arabianSubTorpedoPaths,
      hezbollahPaths,
      attackPathsAll,
    ]
  )

  getBoundsForFilterRef.current = getBoundsForFilter
  const strikeFilterBounds = useMemo(
    () => getBoundsForFilter(strikeLegendFilter),
    [strikeLegendFilter, getBoundsForFilter]
  )

  const lincolnLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveLincolnPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveLincolnPaths]
  )
  const fordLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveFordPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveFordPaths]
  )
  const israelLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveIsraelPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveIsraelPaths]
  )
  /** 场景 A：斯里兰卡鱼雷轨迹 — 直线、蓝色虚线（Mk 48 ADCAP 声纳制导路径） */
  const submarineTorpedoLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features:
        strikeLegendFilter === null || strikeLegendFilter === 'virginia_srilanka'
          ? effectiveVirginiaSrilankaPathsDisplay.map((coords) => ({
              type: 'Feature' as const,
              properties: { weapon: 'Mk 48 Torpedo', status: 'Impact Confirmed' },
              geometry: { type: 'LineString' as const, coordinates: coords },
            }))
          : [],
    }),
    [effectiveVirginiaSrilankaPathsDisplay, strikeLegendFilter]
  )
  /** 场景 B：阿拉伯海战斧弹道 — 抛物线（阿巴斯港/沙希德·鲁德基号） */
  const tomahawkStrikeLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features:
        strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian'
          ? effectiveTomahawkArabianPathsDisplay.map((coords) => ({
              type: 'Feature' as const,
              properties: { target: 'Drone Carrier Shahid Roudaki', damage: 'Critical/Sunk' },
              geometry: { type: 'LineString' as const, coordinates: coords },
            }))
          : [],
    }),
    [effectiveTomahawkArabianPathsDisplay, strikeLegendFilter]
  )
  /** 场景 B：阿巴斯港同艇潜射/鱼雷 — 直线（与战斧同时） */
  const arabianSubTorpedoLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features:
        strikeLegendFilter === null || strikeLegendFilter === 'tomahawk_arabian'
          ? effectiveArabianSubTorpedoPathsDisplay.map((coords) => ({
              type: 'Feature' as const,
              properties: { weapon: 'Sub torpedo', status: 'Impact Confirmed' },
              geometry: { type: 'LineString' as const, coordinates: coords },
            }))
          : [],
    }),
    [effectiveArabianSubTorpedoPathsDisplay, strikeLegendFilter]
  )
  /** 战场态势标注：红圈×斯里兰卡外海驱逐舰沉没；紫色◆阿巴斯港高价值目标摧毁 */
  const tacticalSymbolsGeoJson = useMemo(() => {
    const showAll = strikeLegendFilter === null
    const showVirginia = strikeLegendFilter === 'virginia_srilanka'
    const showTomahawk = strikeLegendFilter === 'tomahawk_arabian'
    const features: GeoJSON.Feature<GeoJSON.Point, { symbol: string; name: string; target?: string; damage?: string }>[] = []
    if (showAll || showVirginia) {
      features.push({
        type: 'Feature' as const,
        properties: { symbol: 'destruction', name: 'Destroyed Surface Combatant' },
        geometry: { type: 'Point' as const, coordinates: [79.8, 6.5] as [number, number] },
      })
    }
    if (showAll || showTomahawk) {
      features.push({
        type: 'Feature' as const,
        properties: {
          symbol: 'explosion',
          name: 'Port Facility/High Value Asset Neutralized',
          target: 'Drone Carrier Shahid Roudaki',
          damage: 'Critical/Sunk',
        },
        geometry: { type: 'Point' as const, coordinates: [56.26, 27.18] as [number, number] },
      })
    }
    return { type: 'FeatureCollection' as const, features }
  }, [strikeLegendFilter])
  /** 盟军打击发起点地标（林肯/福特/以色列/斯里兰卡潜艇/阿拉伯海潜艇）：样式统一，仅阵营颜色不同 */
  const alliedStrikeSourcesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: strikeSources.map((s) => ({
        type: 'Feature' as const,
        properties: { id: s.id, name: s.name },
        geometry: { type: 'Point' as const, coordinates: [s.lng, s.lat] as [number, number] },
      })),
    }),
    [strikeSources]
  )

  /** 盟军打击目标点位（林肯/福特/以色列→伊朗+以色列→黎巴嫩+斯里兰卡/阿巴斯港）。全部/单项：显示全部目标；实时：仅 effectiveCutoffDays 内 */
  const alliedStrikeTargetsFeatures = useMemo(() => {
    const out: GeoJSON.Feature<GeoJSON.Point, { name: string; decay: number }>[] = []
    const onlyRealtimeFilter =
      strikeLegendFilter === null && isRealtimeView
    for (const line of strikeLines) {
      if (
        strikeLegendFilter != null &&
        strikeLegendFilter !== 'lincoln' &&
        strikeLegendFilter !== 'ford' &&
        strikeLegendFilter !== 'israel' &&
        strikeLegendFilter !== 'virginia_srilanka' &&
        strikeLegendFilter !== 'tomahawk_arabian' &&
        strikeLegendFilter !== 'arabian_sea_sub_torpedo'
      )
        continue
      if (strikeLegendFilter === 'lincoln' && line.sourceId !== 'lincoln') continue
      if (strikeLegendFilter === 'ford' && line.sourceId !== 'ford') continue
      if (strikeLegendFilter === 'israel' && line.sourceId !== 'israel') continue
      if (strikeLegendFilter === 'virginia_srilanka' && line.sourceId !== 'virginia_srilanka') continue
      if (strikeLegendFilter === 'tomahawk_arabian' && line.sourceId !== 'tomahawk_arabian' && line.sourceId !== 'arabian_sea_sub_torpedo') continue
      if (strikeLegendFilter === 'arabian_sea_sub_torpedo' && line.sourceId !== 'arabian_sea_sub_torpedo') continue
      for (const t of line.targets) {
        if (onlyRealtimeFilter && !isWithinAnimationWindow((t as { struck_at?: string | null }).struck_at ?? null, referenceTime, effectiveCutoffDays))
          continue
        const struckAt = (t as { struck_at?: string | null }).struck_at ?? null
        const decay = onlyRealtimeFilter
          ? getDecayFactor(struckAt, referenceTime, effectiveCutoffDays)
          : 1
        out.push({
          type: 'Feature',
          properties: { name: (t as { name?: string }).name ?? '', decay },
          geometry: { type: 'Point', coordinates: [t.lng, t.lat] },
        })
      }
    }
    return out
  }, [strikeLines, referenceTime, strikeLegendFilter, isRealtimeView, effectiveCutoffDays])
  const hezbollahLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveHezbollahPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveHezbollahPaths]
  )

  const hormuzLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveHormuzPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveHormuzPaths]
  )

  const attackLinesGeoJson = useMemo(
    () => ({
      type: 'FeatureCollection' as const,
      features: effectiveAttackPaths.map((coords) => ({
        type: 'Feature' as const,
        properties: {},
        geometry: { type: 'LineString' as const, coordinates: coords },
      })),
    }),
    [effectiveAttackPaths]
  )

  // 真主党当前攻击目标点
  const hezbollahTargetsGeoJson = useMemo(
    () =>
      isReplayMode
        ? { type: 'FeatureCollection' as const, features: [] }
        : {
            type: 'FeatureCollection' as const,
            features: EXTENDED_WAR_ZONES.activeAttacks.map((t) => ({
              type: 'Feature' as const,
              properties: { name: t.name, type: t.type, damage: t.damage },
              geometry: { type: 'Point' as const, coordinates: t.coords },
            })),
          },
    [isReplayMode]
  )

  // 霍尔木兹海峡被持续打击的海面目标（用于脉冲与标记）
  const hormuzTargetsGeoJson = useMemo(
    () =>
      isReplayMode
        ? { type: 'FeatureCollection' as const, features: [] }
        : {
            type: 'FeatureCollection' as const,
            features: hormuzTargetPoints.map((coords, idx) => ({
              type: 'Feature' as const,
              properties: { id: `H${idx + 1}` },
              geometry: { type: 'Point' as const, coordinates: coords },
            })),
          },
    [hormuzTargetPoints, isReplayMode]
  )

  // 霍尔木兹海峡交战区 & 真主党势力范围（静态面）；保持引用稳定，避免广播更新时触发 setData 导致轮廓线丢失
  const hormuzZone = useMemo(() => EXTENDED_WAR_ZONES.hormuzCombatZone, [])
  const hezbollahZone = useMemo(() => EXTENDED_WAR_ZONES.hezbollahZone, [])
  const hormuzLabelData = useMemo(
    () => ({
      type: 'Feature' as const,
      properties: { name: (EXTENDED_WAR_ZONES.hormuzCombatZone.properties as { name?: string }).name ?? '霍尔木兹海峡交战区' },
      geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.hormuzLabelCenter },
    }),
    []
  )
  const hezbollahLabelData = useMemo(
    () => ({
      type: 'Feature' as const,
      properties: { name: (EXTENDED_WAR_ZONES.hezbollahZone.properties as { name?: string }).name ?? '真主党势力范围' },
      geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.hezbollahLabelCenter },
    }),
    []
  )
  const kurdishLabelData = useMemo(
    () => ({
      type: 'Feature' as const,
      properties: { name: '库尔德武装' },
      geometry: { type: 'Point' as const, coordinates: EXTENDED_WAR_ZONES.kurdishLabelCenter },
    }),
    []
  )

  // GDELT 冲突事件：1–3 绿, 4–6 橙闪, 7–10 红脉
  const { conflictEventsGreen, conflictEventsOrange, conflictEventsRed } = useMemo(() => {
    const green: GeoJSON.Feature<GeoJSON.Point>[] = []
    const orange: GeoJSON.Feature<GeoJSON.Point>[] = []
    const red: GeoJSON.Feature<GeoJSON.Point>[] = []
    for (const e of conflictEvents) {
      const score = e.impact_score ?? 1
      const f: GeoJSON.Feature<GeoJSON.Point> = {
        type: 'Feature',
        properties: { event_id: e.event_id, impact_score: score },
        geometry: { type: 'Point', coordinates: [e.lng, e.lat] },
      }
      if (score <= 3) green.push(f)
      else if (score <= 6) orange.push(f)
      else red.push(f)
    }
    return {
      conflictEventsGreen: { type: 'FeatureCollection' as const, features: green },
      conflictEventsOrange: { type: 'FeatureCollection' as const, features: orange },
      conflictEventsRed: { type: 'FeatureCollection' as const, features: red },
    }
  }, [conflictEvents])

  const hideNonBelligerentLabels = (map: MapboxMap) => {
    const labelLayers = [
      'country-label',
      'state-label',
      'place-label',
      'place-label-capital',
      'place-label-city',
      'place-label-town',
      'place-label-village',
      'poi-label',
    ]
    for (const id of labelLayers) {
      try {
        if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none')
      } catch (_) {}
    }
  }

  const initAnimation = useRef<(map: MapboxMap) => void>(null!)
  const [zoneSourceKey, setZoneSourceKey] = useState(0)
  const lastCheckedZoneRef = useRef<string>('')
  initAnimation.current = (map: MapboxMap) => {
    startRef.current = performance.now()

    const tick = (t: number) => {
      const elapsed = t - startRef.current
      const intervalMs = getAnimIntervalMs()
      const shouldUpdate = t - lastAnimUpdateRef.current >= intervalMs
      if (shouldUpdate) lastAnimUpdateRef.current = t

      if (shouldUpdate) {
        const zoom = map.getZoom()
        const zoomScale = Math.max(0.4, Math.min(1.6, zoom / 4.2)) // 镜头拉近效果变大、拉远脉冲半径变小
        // 全部 / 单项选中：完整动画不受衰减；实时：按时间衰减
        const soloMode =
          strikeLegendFilterRef.current != null || !isRealtimeViewRef.current
        const decay = soloMode ? 1 : Math.max(0.2, animationDecayRef.current)
        const decayScale = soloMode ? 1 : (0.3 + 0.7 * decay) // 衰减强度线性插值：decay 低则脉冲半径缩小
        const step = soloMode ? 1 : Math.max(1, Math.round(1 / decay)) // 频次：单选时每条路径都显示光点
        try {
          // 伊朗→美军基地：速度固定，仅减少光点数量（频次）
        const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
        const paths = attackPathsRef.current
        if (src && paths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          paths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + i / Math.max(paths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          src.setData({ type: 'FeatureCollection', features })
        }
        // damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
        if (map.getLayer('points-damaged')) {
          const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.003)
          map.setPaintProperty('points-damaged', 'circle-opacity', blink)
        }
        // 脉冲：仅更新半径与透明度，颜色使用 Layer 默认（美军基地遭袭=红，盟军打击伊朗目标=蓝）
        if (map.getLayer('points-attacked-pulse')) {
          const cycle = 2000
          const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
          const r = Math.max(0, 32 * phase * zoomScale * decayScale)
          const opacity = Math.min(1, Math.max(0, (0.4 + 0.6 * decay) * (1 - phase)))
          map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
          map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
        }
        // 林肯号打击伊朗：蓝色光点
        const lincolnSrc = map.getSource('allied-strike-dots-lincoln') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const lincolnPaths = lincolnPathsRef.current
        if (lincolnSrc && lincolnPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          lincolnPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(lincolnPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          lincolnSrc.setData({ type: 'FeatureCollection', features })
        }
        // 福特号打击伊朗：青色光点
        const fordSrc = map.getSource('allied-strike-dots-ford') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const fordPaths = fordPathsRef.current
        if (fordSrc && fordPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          fordPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.3 + i / Math.max(fordPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          fordSrc.setData({ type: 'FeatureCollection', features })
        }
        // 以色列打击伊朗：浅青/白色光点
        const israelSrc = map.getSource('allied-strike-dots-israel') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const israelPaths = israelPathsRef.current
        if (israelSrc && israelPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          israelPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.1 + i / Math.max(israelPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          israelSrc.setData({ type: 'FeatureCollection', features })
        }
        // 斯里兰卡伏击：鱼雷轨迹蓝色光点（直线路径）
        const virginiaSrc = map.getSource('allied-strike-dots-virginia') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const virginiaPaths = virginiaPathsRef.current
        if (virginiaSrc && virginiaPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          virginiaPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.6 + i / Math.max(virginiaPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          virginiaSrc.setData({ type: 'FeatureCollection', features })
        }
        // 阿巴斯港突袭：战斧弹道紫色光点（抛物线路径）
        const tomahawkSrc = map.getSource('allied-strike-dots-tomahawk') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const tomahawkPaths = tomahawkPathsRef.current
        if (tomahawkSrc && tomahawkPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          tomahawkPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.4 + i / Math.max(tomahawkPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          tomahawkSrc.setData({ type: 'FeatureCollection', features })
        }
        // 阿巴斯港同艇潜射/鱼雷光点（直线路径，与战斧同时）
        const arabianSubSrc = map.getSource('allied-strike-dots-arabian-sub') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const arabianSubPaths = arabianSubTorpedoPathsRef.current
        if (arabianSubSrc && arabianSubPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          arabianSubPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(arabianSubPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          arabianSubSrc.setData({ type: 'FeatureCollection', features })
        }
        // 真主党打击以色列北部：橙红光点，与林肯/福特/以色列同一动画方式
        const hezSrc = map.getSource('hezbollah-strike-dots') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const hezPaths = hezbollahPathsRef.current
        if (hezSrc && hezPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          hezPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.2 + i / Math.max(hezPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          hezSrc.setData({ type: 'FeatureCollection', features })
        }
        // 伊朗对霍尔木兹海峡：黄色光点，沿海峡方向飞行
        const hormuzSrc = map.getSource('iran-hormuz-dots') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const hormuzPaths = hormuzPathsRef.current
        if (hormuzSrc && hormuzPaths.length > 0) {
          const features: GeoJSON.Feature<GeoJSON.Point>[] = []
          hormuzPaths.forEach((path, i) => {
            if (i % step !== 0) return
            const progress = (elapsed / FLIGHT_DURATION_MS + 0.15 + i / Math.max(hormuzPaths.length, 1)) % 1
            features.push({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: interpolateOnPath(path, progress) },
            })
          })
          hormuzSrc.setData({ type: 'FeatureCollection', features })
        }
        // 盟军打击目标（伊朗境内等）：脉冲动画，颜色保持 Layer 默认蓝
        if (map.getLayer('allied-strike-targets-pulse')) {
          const cycle = 2000
          const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
          const breathMin = 8
          const breathMax = 26
          const r = Math.max(0, (breathMin + (breathMax - breathMin) * decay) * phase * zoomScale * decayScale)
          const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.15)))
          map.setPaintProperty('allied-strike-targets-pulse', 'circle-radius', r)
          map.setPaintProperty('allied-strike-targets-pulse', 'circle-opacity', opacity)
        }
        // 单箭头钳形动画：生长(缓动) → 保持 2s 循环（已去掉内发光闪烁）
        const pincerGrowthSrc = map.getSource('kurdish-pincer-growth') as
          | { setData: (d: GeoJSON.FeatureCollection) => void }
          | undefined
        const PINCER_GROW_MS = 2500
        const PINCER_HOLD_MS = 2000
        const PINCER_CYCLE_MS = PINCER_GROW_MS + PINCER_HOLD_MS
        const pincerInCycle = elapsed % PINCER_CYCLE_MS
        const isGrowth = pincerInCycle < PINCER_GROW_MS
        const growthT = isGrowth ? pincerInCycle / PINCER_GROW_MS : 1
        const progressEased = growthT * growthT * (3 - 2 * growthT)
        const pincerProgress = isGrowth ? progressEased : 1

        // 节流：仅当 progress 步进变化或进入保持阶段时更新 GeoJSON，减轻首帧卡顿与每帧 setData 开销
        const progressStep = Math.floor(pincerProgress * 40) / 40
        const lastStep = pincerAnimRef.current.lastProgressStep ?? -1
        const shouldUpdate = progressStep !== lastStep || (!isGrowth && lastStep !== 1)
        if (shouldUpdate) {
          pincerAnimRef.current.lastProgressStep = isGrowth ? progressStep : 1
          const progressToUse = isGrowth ? pincerProgress : 1
          if (pincerGrowthSrc && warMapData.pincerAxes.length > 0) {
            const features: GeoJSON.Feature<GeoJSON.Polygon>[] = warMapData.pincerAxes.map((axis) => ({
              type: 'Feature',
              properties: { name: axis.name },
              geometry: {
                type: 'Polygon',
                coordinates: [createTacticalPincerAtProgress(axis.start, axis.end, progressToUse)],
              },
            }))
            pincerGrowthSrc.setData({ type: 'FeatureCollection', features })
          }
          const israelLebanonSrc = map.getSource('israel-lebanon-arrow') as
            | { setData: (d: GeoJSON.FeatureCollection) => void }
            | undefined
          if (israelLebanonSrc) {
            const { start, end, name } = warMapData.israelLebanonAxis
            israelLebanonSrc.setData({
              type: 'FeatureCollection',
              features: [{
                type: 'Feature',
                properties: { name },
                geometry: {
                  type: 'Polygon',
                  coordinates: [createTacticalPincerAtProgress(start, end, progressToUse)],
                },
              }],
            })
          }
        }
        if (map.getLayer('attack-pincers')) {
          map.setPaintProperty('attack-pincers', 'fill-opacity', 1)
        }
        if (map.getLayer('attack-pincers-inner-glow')) {
          map.setPaintProperty('attack-pincers-inner-glow', 'fill-opacity', 0)
        }
        if (map.getLayer('israel-lebanon-arrow-fill')) {
          map.setPaintProperty('israel-lebanon-arrow-fill', 'fill-opacity', 1)
        }
        if (map.getLayer('israel-lebanon-arrow-inner-glow')) {
          map.setPaintProperty('israel-lebanon-arrow-inner-glow', 'fill-opacity', 0)
        }
        // GDELT 橙色 (4–6)：闪烁
        if (map.getLayer('gdelt-events-orange')) {
          const blink = 0.5 + 0.5 * Math.sin(elapsed * 0.004)
          map.setPaintProperty('gdelt-events-orange', 'circle-opacity', blink)
        }
        // GDELT 高烈度：脉冲动画，颜色保持 Layer 默认
        if (map.getLayer('gdelt-events-red-pulse')) {
          const cycle = 2200
          const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
          const r = Math.max(0, 24 * phase * zoomScale * decayScale)
          const opacity = Math.min(1, Math.max(0, (0.4 + 0.6 * decay) * (1 - phase * 1.05)))
          map.setPaintProperty('gdelt-events-red-pulse', 'circle-radius', r)
          map.setPaintProperty('gdelt-events-red-pulse', 'circle-opacity', opacity)
        }
        // 真主党攻击目标：脉冲动画，颜色保持 Layer 默认
        if (map.getLayer('hezbollah-attack-targets-pulse')) {
          const cycle = 2000
          const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
          const r = Math.max(0, 26 * phase * zoomScale * decayScale)
          const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.1)))
          map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-radius', r)
          map.setPaintProperty('hezbollah-attack-targets-pulse', 'circle-opacity', opacity)
        }
        // 霍尔木兹海峡被打击目标：脉冲动画，颜色保持 Layer 默认
        if (map.getLayer('iran-hormuz-targets-pulse')) {
          const cycle = 2000
          const phase = Math.max(0, Math.min(1, (elapsed % cycle) / cycle))
          const r = Math.max(0, 24 * phase * zoomScale * decayScale)
          const opacity = Math.min(1, Math.max(0, (0.35 + 0.65 * decay) * (1 - phase * 1.05)))
          map.setPaintProperty('iran-hormuz-targets-pulse', 'circle-radius', r)
          map.setPaintProperty('iran-hormuz-targets-pulse', 'circle-opacity', opacity)
        }
        } catch (_) {}
      }
      animRef.current = requestAnimationFrame(tick)
    }

    const start = () => {
      hideNonBelligerentLabels(map)
      map.fitBounds(
        [[THEATER_BBOX[0], THEATER_BBOX[1]], [THEATER_BBOX[2], THEATER_BBOX[3]]],
        { padding: 40, maxZoom: 5, duration: 0 }
      )
      const hasAnim =
        (map.getSource('attack-dots') && attackPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-virginia') && virginiaPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-tomahawk') && tomahawkPathsRef.current.length > 0) ||
        (map.getSource('allied-strike-dots-arabian-sub') && arabianSubTorpedoPathsRef.current.length > 0) ||
        (map.getSource('hezbollah-strike-dots') && hezbollahPathsRef.current.length > 0) ||
        (map.getSource('iran-hormuz-dots') && hormuzPathsRef.current.length > 0) ||
        map.getSource('kurdish-pincer-growth') ||
        map.getSource('israel-lebanon-arrow') ||
        map.getSource('gdelt-events-green') ||
        map.getSource('gdelt-events-orange') ||
        map.getSource('gdelt-events-red')
      if (hasAnim) {
        animRef.current = requestAnimationFrame(tick)
      } else {
        animRef.current = requestAnimationFrame(start)
      }
    }
    start()
  }

  useEffect(() => {
    return () => cancelAnimationFrame(animRef.current)
  }, [])

  // 主视角：容器尺寸变化时无动画；图例点「全部/实时」时平滑飞回；移动端减小 padding、缩短 duration
  const fitToTheater = useCallback((options?: { duration?: number }) => {
    const map = mapRef.current?.getMap()
    if (!map) return
    const duration = options?.duration ?? 0
    const isMobile = typeof window !== 'undefined' && window.matchMedia('(max-width: 640px)').matches
    const padding = duration > 0 && isMobile ? 24 : 32
    const flyDuration = duration > 0 && isMobile ? 400 : duration
    map.fitBounds(THEATER_BOUNDS, { padding, maxZoom: 6, duration: flyDuration })
  }, [])

  useEffect(() => {
    const el = containerRef.current
    if (!el) return
    const ro = new ResizeObserver(() => fitToTheater())
    ro.observe(el)
    return () => ro.disconnect()
  }, [fitToTheater])

  // 势力/区域选中时：只显示该势力填充层，其余填充层隐藏（与「林肯打击」只显示该路径同逻辑）
  const ZONE_FILL_LAYERS = ['iran-fill', 'hormuz-combat-fill', 'hezbollah-fill', 'kurdish-zones'] as const
  const zoneOnlyVisibility = useMemo(() => {
    const f = strikeLegendFilter
    if (f === 'iran') return { 'iran-fill': true, 'hormuz-combat-fill': false, 'hezbollah-fill': false, 'kurdish-zones': false }
    if (f === 'hormuz') return { 'iran-fill': false, 'hormuz-combat-fill': true, 'hezbollah-fill': false, 'kurdish-zones': false }
    if (f === 'hezbollahZone') return { 'iran-fill': false, 'hormuz-combat-fill': false, 'hezbollah-fill': true, 'kurdish-zones': false }
    if (f === 'kurdish') return { 'iran-fill': false, 'hormuz-combat-fill': false, 'hezbollah-fill': false, 'kurdish-zones': true }
    return null
  }, [strikeLegendFilter])
  useEffect(() => {
    const map = mapRef.current?.getMap()
    if (!map?.isStyleLoaded() || zoneOnlyVisibility == null) return
    for (const id of ZONE_FILL_LAYERS) {
      try {
        if (map.getLayer(id))
          map.setLayoutProperty(id, 'visibility', zoneOnlyVisibility[id] ? 'visible' : 'none')
      } catch (_) {}
    }
  }, [zoneOnlyVisibility])
  useEffect(() => {
    if (zoneOnlyVisibility != null) return
    const map = mapRef.current?.getMap()
    if (!map?.isStyleLoaded()) return
    for (const id of ZONE_FILL_LAYERS) {
      try {
        if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'visible')
      } catch (_) {}
    }
  }, [zoneOnlyVisibility])

  // 广播更新后检查轮廓层是否仍在；若被误删则递进 zoneSourceKey 强制轮廓 Source 重新挂载
  useEffect(() => {
    const lastUpdated = situation.lastUpdated ?? ''
    if (lastUpdated === lastCheckedZoneRef.current) return
    lastCheckedZoneRef.current = lastUpdated
    const map = mapRef.current?.getMap()
    if (!map?.isStyleLoaded()) return
    const t = setTimeout(() => {
      const map2 = mapRef.current?.getMap()
      if (!map2) return
      const hasKurdish = !!map2.getLayer('kurdish-zones')
      const hasHormuz = !!map2.getLayer('hormuz-combat-fill')
      const hasHezbollah = !!map2.getLayer('hezbollah-fill')
      if (!hasKurdish || !hasHormuz || !hasHezbollah) {
        setZoneSourceKey((k) => k + 1)
      }
    }, 150)
    return () => clearTimeout(t)
  }, [situation.lastUpdated])

  if (!MAPBOX_TOKEN) {
    return (
      <div className="flex h-full w-full items-center justify-center bg-military-dark">
        <div className="rounded-lg border border-military-border bg-military-panel/95 p-8 text-center shadow-lg">
          <p className="font-orbitron text-sm font-medium text-military-text-primary">地图需要 Mapbox 令牌</p>
          <p className="mt-2 text-xs text-military-text-secondary">
            复制 .env.example 为 .env，填入令牌后重启
          </p>
          <a
            href="https://account.mapbox.com/access-tokens/"
            target="_blank"
            rel="noopener noreferrer"
            className="mt-1 inline-block text-[10px] text-military-accent hover:underline"
          >
            免费申请 Mapbox 令牌 →
          </a>
        </div>
      </div>
    )
  }

  return (
    <div ref={containerRef} className="relative h-full w-full min-w-0">
      {/* 打击交互面板：移动端可收纳，默认展开；桌面端始终显示 */}
      <div className="absolute bottom-2 left-2 z-10 flex flex-col items-start gap-1">
        {/* 移动端收纳后：展开按钮 */}
        <button
          type="button"
          className="sm:hidden flex items-center gap-1 rounded bg-black/70 px-2 py-1.5 text-[9px] text-white border border-white/20 hover:bg-black/85"
          onClick={() => setStrikePanelOpen(true)}
          style={{ display: strikePanelOpen ? 'none' : 'flex' }}
          aria-label="展开打击面板"
        >
          <span className="text-cyan-300">打击</span>
          <span className="opacity-70">▸</span>
        </button>
        {/* 面板主体：移动端收纳时隐藏 */}
        <div
          className={`flex flex-wrap gap-x-3 gap-y-1 rounded bg-black/70 px-2 py-1.5 text-[9px] sm:text-[10px] ${!strikePanelOpen ? 'max-sm:hidden' : ''}`}
        >
        <span className="flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" /> 基地
        </span>
        <span className="flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 遭袭
        </span>
        <span className="flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 海军
        </span>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('iran')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'iran' ? 'ring-1 ring-[#EF4444] bg-[#EF4444]/20' : 'hover:bg-white/10'}`}
          title="只显示伊朗相关"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" /> 伊朗
        </button>
        <span className="flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 胡塞武装
        </span>
        <span className="flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-sm bg-red-500/40" /> 苏丹武装
        </span>
        <button
          type="button"
          onClick={() => {
            strikeLegendFilterRef.current = null
            isRealtimeViewRef.current = false
            setStrikeLegendFilter(null)
            setIsRealtimeView(false)
            fitToTheater({ duration: 500 })
          }}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === null && !isRealtimeView ? 'ring-1 ring-cyan-400/80 bg-cyan-400/10' : 'hover:bg-white/10'}`}
          title="主视角，显示全部打击动画"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-military-text-secondary" /> 全部
        </button>
        <button
          type="button"
          onClick={() => {
            strikeLegendFilterRef.current = null
            isRealtimeViewRef.current = true
            setStrikeLegendFilter(null)
            setIsRealtimeView(true)
            fitToTheater({ duration: 500 })
          }}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === null && isRealtimeView ? 'ring-1 ring-amber-400/80 bg-amber-400/10' : 'hover:bg-white/10'}`}
          title="主视角，仅渲染最新进展（近 5 天）"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-amber-400" /> 实时
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('lincoln')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'lincoln' ? 'ring-1 ring-[#3B82F6] bg-[#3B82F6]/20' : 'hover:bg-white/10'}`}
          title="只显示林肯打击"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-[#3B82F6]" /> 林肯打击
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('ford')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'ford' ? 'ring-1 ring-[#06B6D4] bg-[#06B6D4]/20' : 'hover:bg-white/10'}`}
          title="只显示福特打击"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-[#06B6D4]" /> 福特打击
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('israel')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'israel' ? 'ring-1 ring-[#22D3EE] bg-[#22D3EE]/20' : 'hover:bg-white/10'}`}
          title="只显示以色列打击"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-[#22D3EE]" /> 以色列打击
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('virginia_srilanka')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'virginia_srilanka' ? 'ring-1 ring-blue-400 bg-blue-400/20' : 'hover:bg-white/10'}`}
          title="斯里兰卡海域深海伏击 — 鱼雷轨迹"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-blue-400" /> 斯里兰卡伏击
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('tomahawk_arabian')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'tomahawk_arabian' ? 'ring-1 ring-purple-400 bg-purple-400/20' : 'hover:bg-white/10'}`}
          title="阿巴斯港/沙希德·鲁德基号远程打击"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-purple-400" /> 阿巴斯港突袭
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('hormuz')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'hormuz' ? 'ring-1 ring-yellow-400 bg-yellow-400/20' : 'hover:bg-white/10'}`}
          title="只显示霍尔木兹交战区"
        >
          <span className="h-1.5 w-1.5 rounded-sm bg-yellow-400/50" /> 霍尔木兹交战区
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('hezbollahZone')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'hezbollahZone' ? 'ring-1 ring-lime-400 bg-lime-400/20' : 'hover:bg-white/10'}`}
          title="只显示真主党势力"
        >
          <span className="h-1.5 w-1.5 rounded-sm bg-lime-400/40" /> 真主党势力
        </button>
        <button
          type="button"
          onClick={() => toggleStrikeFilter('kurdish')}
          className={`flex items-center gap-1 rounded px-1 py-0.5 transition-colors ${strikeLegendFilter === 'kurdish' ? 'ring-1 ring-[#E066FF] bg-[#E066FF]/20' : 'hover:bg-white/10'}`}
          title="只显示库尔德武装"
        >
          <span className="h-1.5 w-1.5 rounded-sm bg-[#E066FF]/80" /> 库尔德武装
        </button>
        {/* 移动端收纳按钮 */}
        <button
          type="button"
          className="sm:hidden shrink-0 self-center rounded p-0.5 text-white/70 hover:bg-white/10 hover:text-white"
          onClick={() => setStrikePanelOpen(false)}
          aria-label="收纳打击面板"
          title="收纳面板以展示地图"
        >
          <span className="text-[10px]">▼</span>
        </button>
        </div>
      </div>
      {/* 右侧图例模块：可收纳悬浮按钮 */}
      <div className="absolute right-2 top-1/2 z-10 -translate-y-1/2">
        {legendOpen ? (
          <div className="flex flex-col gap-1 rounded bg-black/80 px-2 py-2 text-[9px] sm:text-[10px] shadow-lg border border-white/10">
            <div className="mb-1 flex items-center justify-between gap-2 border-b border-white/10 pb-1">
              <span className="text-[10px] font-semibold text-cyan-300">战场图例</span>
              <button
                type="button"
                onClick={() => setLegendOpen(false)}
                className="h-4 w-4 flex items-center justify-center rounded-full border border-cyan-400/60 text-[9px] text-cyan-300 hover:bg-cyan-400/20"
                aria-label="收起图例"
              >
                ×
              </button>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-px w-4 bg-yellow-300" />
              <span className="text-[9px] text-military-text-secondary">伊朗西部防御线</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-2 w-3 rotate-[-20deg] border-b-2 border-l-2 border-blue-400" />
              <span className="text-[9px] text-military-text-secondary">库尔德攻势 / 进攻箭头</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-2 w-2 rounded-full border border-blue-400 bg-blue-500/60" />
              <span className="text-[9px] text-military-text-secondary">美国/盟军打击目标</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-2 w-2 rounded-full border border-amber-300 bg-amber-300/60" />
              <span className="text-[9px] text-military-text-secondary">霍尔木兹海峡交战区</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" />
              <span className="text-[9px] text-military-text-secondary">美军基地 / 低烈度 GDELT</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" />
              <span className="text-[9px] text-military-text-secondary">伊朗遭袭目标 / 高烈度事件</span>
            </div>
            <div className="mt-1 border-t border-white/10 pt-1">
              <span className="text-[9px] text-military-text-secondary">GDELT 烈度</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-1.5 w-1.5 rounded-full bg-[#22C55E]" />
              <span className="text-[9px] text-military-text-secondary">低烈度</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-1.5 w-1.5 rounded-full bg-[#F97316]" />
              <span className="text-[9px] text-military-text-secondary">中烈度</span>
            </div>
            <div className="flex items-center gap-1">
              <span className="h-1.5 w-1.5 rounded-full bg-[#EF4444]" />
              <span className="text-[9px] text-military-text-secondary">高烈度</span>
            </div>
          </div>
        ) : (
          <button
            type="button"
            onClick={() => setLegendOpen(true)}
            className="flex items-center gap-1 rounded-full bg-black/80 px-2 py-1 text-[9px] text-cyan-300 shadow-lg border border-cyan-400/60 hover:bg-cyan-500/10"
            aria-label="展开战场图例"
          >
            <span className="h-2 w-2 rounded-full bg-cyan-400/80" />
            <span>图例</span>
          </button>
        )}
      </div>
      <Map
        ref={mapRef}
        initialViewState={DEFAULT_VIEW}
        mapStyle="mapbox://styles/mapbox/dark-v11"
        mapboxAccessToken={MAPBOX_TOKEN}
        attributionControl={false}
        dragRotate={false}
        touchRotate={false}
        style={{ width: '100%', height: '100%' }}
        onLoad={(e) => {
          fitToTheater()
          // 等地图 style/tiles 就绪后再启动动画，减轻首帧卡顿
          const map = e.target
          if (map.isStyleLoaded()) {
            map.once('idle', () => initAnimation.current(map))
          } else {
            map.once('load', () => map.once('idle', () => initAnimation.current(map)))
          }
        }}
      >
        {/* 矢量标记：zoom 拉远变小，拉近变大 */}
        <Source id="points-us-naval" type="geojson" data={usNaval}>
          <Layer
            id="points-us-naval"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
              'circle-color': '#3B82F6',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        <Source id="points-us-base-op" type="geojson" data={usBaseOp}>
          <Layer
            id="points-us-base-op"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
              'circle-color': '#22C55E',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        {/* 伊朗对霍尔木兹海峡的打击路径（黄色轨迹） */}
        <Source id="iran-hormuz-lines" type="geojson" data={hormuzLinesGeoJson}>
          <Layer
            id="iran-hormuz-lines"
            type="line"
            paint={{
              'line-color': 'rgba(250, 204, 21, 0.55)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.6, 8, 1.2, 12, 2],
            }}
          />
        </Source>
        {/* 伊朗对霍尔木兹的打击光点（黄色） */}
        <Source
          id="iran-hormuz-dots"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: effectiveHormuzPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="iran-hormuz-dots-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 5.5, 12, 9],
              'circle-color': 'rgba(250, 204, 21, 0.65)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="iran-hormuz-dots-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1.2, 8, 2.2, 12, 3.8],
              'circle-color': '#facc15',
              'circle-stroke-width': 0.6,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        {/* 霍尔木兹海峡被打击目标点 + 脉冲（与其他被打击点风格一致，颜色区分为琥珀黄） */}
        <Source id="iran-hormuz-targets" type="geojson" data={hormuzTargetsGeoJson}>
          <Layer
            id="iran-hormuz-targets-dot"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 3.5, 12, 5],
              'circle-color': '#fbbf24',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="iran-hormuz-targets-pulse"
            type="circle"
            paint={{
              'circle-radius': 0,
              'circle-color': 'rgba(251, 191, 36, 0.45)',
              'circle-opacity': 0,
            }}
          />
        </Source>

        <Source id="points-us-base-damaged" type="geojson" data={usBaseDamaged}>
          <Layer
            id="points-damaged"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
              'circle-color': '#F97316',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
              'circle-stroke-color': '#fff',
              'circle-opacity': 1,
            }}
          />
        </Source>

        <Source id="points-us-base-attacked" type="geojson" data={usBaseAttacked}>
          <Layer
            id="points-attacked-dot"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
              'circle-color': '#EF4444',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="points-attacked-pulse"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 12, 12, 24],
              'circle-color': '#EF4444',
              'circle-opacity': 0,
            }}
          />
        </Source>

        <Source
          id="points-iran"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: irLocs.map((loc) => toFeature(loc, 'iran')),
          }}
        >
          <Layer
            id="points-iran"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1, 5, 2, 7, 3.5, 10, 6],
              'circle-color': '#EF4444',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1, 10, 1.5],
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        {/* GDELT 冲突事件：1–3 绿点, 4–6 橙闪, 7–10 红脉 */}
        <Source id="gdelt-events-green" type="geojson" data={conflictEventsGreen}>
          <Layer
            id="gdelt-events-green"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 4, 12, 6],
              'circle-color': '#22C55E',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        <Source id="gdelt-events-orange" type="geojson" data={conflictEventsOrange}>
          <Layer
            id="gdelt-events-orange"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 5, 12, 8],
              'circle-color': '#F97316',
              'circle-opacity': 0.8,
            }}
          />
        </Source>
        <Source id="gdelt-events-red" type="geojson" data={conflictEventsRed}>
          <Layer
            id="gdelt-events-red-dot"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 4, 12, 6],
              'circle-color': '#EF4444',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="gdelt-events-red-pulse"
            type="circle"
            paint={{
              'circle-radius': 0,
              'circle-color': 'rgba(239, 68, 68, 0.5)',
              'circle-opacity': 0,
            }}
          />
        </Source>

        {/* 真主党对以色列北部的攻击矢量线（与林肯/福特/以色列线宽一致） */}
        <Source id="hezbollah-attack-lines" type="geojson" data={hezbollahLinesGeoJson}>
          <Layer
            id="hezbollah-attack-lines"
            type="line"
            paint={{
              'line-color': 'rgba(248, 113, 113, 0.45)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        {/* 真主党打击光点（与林肯/福特/以色列光点半径与动画一致） */}
        <Source
          id="hezbollah-strike-dots"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: hezbollahPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="hezbollah-strike-dots-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(248, 113, 113, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="hezbollah-strike-dots-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#F97316',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        <Source id="hezbollah-attack-targets" type="geojson" data={hezbollahTargetsGeoJson}>
          <Layer
            id="hezbollah-attack-targets-dot"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 2, 8, 3.5, 12, 5],
              'circle-color': '#F97316',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="hezbollah-attack-targets-pulse"
            type="circle"
            paint={{
              'circle-radius': 0,
              'circle-color': 'rgba(248, 113, 113, 0.45)',
              'circle-opacity': 0,
            }}
          />
        </Source>

        {/* 跨国库尔德势力：土(Bakur)/叙(Rojava)/伊(Bashur) 三区 MultiPolygon + 北/南钳形箭头 */}
        <Source key={`zone-kurdish-${zoneSourceKey}`} id="kurdish-front-source" type="geojson" data={KURDISH_FRONT_GEOJSON}>
          {/* 势力范围：紫色半透明，远景更显、近景更透（描边单独 line 层，参考真主党） */}
          <Layer
            id="kurdish-zones"
            type="fill"
            filter={['==', ['get', 'region_type'], 'InfluenceZone']}
            paint={{
              'fill-color': '#800080',
              'fill-opacity': [
                'interpolate',
                ['linear'],
                ['zoom'],
                3,
                0.4,
                8,
                0.15,
              ],
            }}
          />
          <Layer
            id="kurdish-zones-outline"
            type="line"
            filter={['==', ['get', 'region_type'], 'InfluenceZone']}
            paint={{
              'line-color': '#BA55D3',
              'line-width': 1.2,
            }}
          />
          {/* 进攻目的地地名：亮紫色醒目识别 */}
          <Layer
            id="kurdish-target-labels"
            type="symbol"
            filter={['==', ['get', 'region_type'], 'Target']}
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
              'text-anchor': 'top',
              'text-offset': [0, 0.8],
            }}
            paint={{
              'text-color': '#E066FF',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
          {/* 萨南达季、克尔曼沙赫显示圆点（大不里士不显示标记） */}
          <Layer
            id="kurdish-target-dots"
            type="circle"
            filter={['all', ['==', ['get', 'region_type'], 'Target'], ['==', ['get', 'showMarker'], true]]}
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
              'circle-color': '#BF40BF',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        {/* 伊朗被库尔德进攻点防御线：反弓曲线 + 锯齿，黄色；名称标注 */}
        <Source id="kurdish-attack-defense-line" type="geojson" data={warMapData.kurdishDefenseLineGeoJson}>
          <Layer
            id="kurdish-attack-defense-line-layer"
            type="line"
            filter={['==', ['geometry-type'], 'LineString']}
            paint={{
              'line-color': '#FACC15',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 1.5, 12, 2.5],
              'line-join': 'round',
              'line-cap': 'round',
            }}
          />
          <Layer
            id="kurdish-attack-defense-line-label"
            type="symbol"
            filter={['==', ['geometry-type'], 'Point']}
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
              'text-anchor': 'center',
              'text-rotate': ['get', 'angle'],
            }}
            paint={{
              'text-color': '#FACC15',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
        </Source>

        {/* 单箭头钳形：曲线箭体，白色加粗轮廓与美方攻击点样式一致 */}
        <Source id="kurdish-pincer-growth" type="geojson" data={warMapData.kurdishPincerGrowthInitial}>
          <Layer
            id="attack-pincers"
            type="fill"
            paint={{
              'fill-color': '#2563eb',
              'fill-opacity': 1,
              'fill-outline-color': '#FFFFFF',
            }}
          />
          <Layer
            id="attack-pincers-inner-glow"
            type="fill"
            paint={{
              'fill-color': '#60a5fa',
              'fill-opacity': 0,
              'fill-outline-color': 'transparent',
            }}
          />
          <Layer
            id="attack-pincers-outline"
            type="line"
            paint={{
              'line-color': '#FFFFFF',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 7, 1.2, 10, 1.8],
              'line-opacity': 1,
              'line-join': 'round',
              'line-cap': 'round',
            }}
          />
        </Source>

        {/* 以色列进攻黎巴嫩箭头：与库尔德同款曲线生长、白轮廓、内发光 */}
        <Source id="israel-lebanon-arrow" type="geojson" data={warMapData.israelLebanonArrowInitial}>
          <Layer
            id="israel-lebanon-arrow-fill"
            type="fill"
            paint={{
              'fill-color': '#2563eb',
              'fill-opacity': 1,
              'fill-outline-color': '#FFFFFF',
            }}
          />
          <Layer
            id="israel-lebanon-arrow-inner-glow"
            type="fill"
            paint={{
              'fill-color': '#60a5fa',
              'fill-opacity': 0,
              'fill-outline-color': 'transparent',
            }}
          />
          <Layer
            id="israel-lebanon-arrow-outline"
            type="line"
            paint={{
              'line-color': '#FFFFFF',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 7, 1.2, 10, 1.8],
              'line-opacity': 1,
              'line-join': 'round',
              'line-cap': 'round',
            }}
          />
        </Source>

        {/* 盟军打击发起点地标：与目标点样式统一（大小/描边一致），仅颜色区分阵营 */}
        <Source id="allied-strike-sources" type="geojson" data={alliedStrikeSourcesGeoJson}>
          <Layer
            id="allied-strike-sources-circle"
            type="circle"
            filter={
              strikeLegendFilter == null
                ? ['in', ['get', 'id'], ['literal', ['lincoln', 'ford', 'israel', 'virginia_srilanka', 'tomahawk_arabian', 'arabian_sea_sub_torpedo']]]
                : strikeLegendFilter === 'tomahawk_arabian'
                  ? ['in', ['get', 'id'], ['literal', ['tomahawk_arabian', 'arabian_sea_sub_torpedo']]]
                  : ['==', ['get', 'id'], strikeLegendFilter]
            }
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
              'circle-color': [
                'match',
                ['get', 'id'],
                'lincoln',
                '#3B82F6',
                'ford',
                '#06B6D4',
                'israel',
                '#22D3EE',
                'virginia_srilanka',
                '#3B82F6',
                'tomahawk_arabian',
                '#A855F7',
                'arabian_sea_sub_torpedo',
                '#06B6D4',
                '#94A3B8',
              ],
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="allied-strike-sources-label"
            type="symbol"
            filter={
              strikeLegendFilter == null
                ? ['in', ['get', 'id'], ['literal', ['lincoln', 'ford', 'israel', 'virginia_srilanka', 'tomahawk_arabian', 'arabian_sea_sub_torpedo']]]
                : strikeLegendFilter === 'tomahawk_arabian'
                  ? ['in', ['get', 'id'], ['literal', ['tomahawk_arabian', 'arabian_sea_sub_torpedo']]]
                  : ['==', ['get', 'id'], strikeLegendFilter]
            }
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
              'text-anchor': 'top',
              'text-offset': [0, 0.8],
            }}
            paint={{
              'text-color': '#FFFFFF',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
        </Source>

        {/* 美以联军打击伊朗：路径线 */}
        <Source id="allied-strike-lines-lincoln" type="geojson" data={lincolnLinesGeoJson}>
          <Layer
            id="allied-strike-lines-lincoln"
            type="line"
            paint={{
              'line-color': 'rgba(96, 165, 250, 0.45)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        <Source id="allied-strike-lines-ford" type="geojson" data={fordLinesGeoJson}>
          <Layer
            id="allied-strike-lines-ford"
            type="line"
            paint={{
              'line-color': 'rgba(6, 182, 212, 0.45)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        <Source id="allied-strike-lines-israel" type="geojson" data={israelLinesGeoJson}>
          <Layer
            id="allied-strike-lines-israel"
            type="line"
            paint={{
              'line-color': 'rgba(34, 211, 238, 0.45)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        {/* 场景 A：斯里兰卡海域鱼雷轨迹 — 蓝色虚线（Mk 48 ADCAP 声纳制导路径） */}
        <Source id="submarine-torpedo-lines" type="geojson" data={submarineTorpedoLinesGeoJson}>
          <Layer
            id="submarine-torpedo-lines"
            type="line"
            paint={{
              'line-color': 'rgba(59, 130, 246, 0.7)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 8, 1.5, 12, 2.5],
              'line-dasharray': [2, 1.5],
            }}
          />
        </Source>
        {/* 场景 B：阿拉伯海战斧 → 阿巴斯港无人机航母 */}
        <Source id="tomahawk-strike-lines" type="geojson" data={tomahawkStrikeLinesGeoJson}>
          <Layer
            id="tomahawk-strike-lines"
            type="line"
            paint={{
              'line-color': 'rgba(168, 85, 247, 0.5)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        {/* 场景 B：阿巴斯港同艇潜射/鱼雷 — 直线、青虚线（与战斧同时） */}
        <Source id="arabian-sub-torpedo-lines" type="geojson" data={arabianSubTorpedoLinesGeoJson}>
          <Layer
            id="arabian-sub-torpedo-lines"
            type="line"
            paint={{
              'line-color': 'rgba(6, 182, 212, 0.65)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.6, 8, 1.2, 12, 2],
              'line-dasharray': [2, 1.5],
            }}
          />
        </Source>
        {/* 战术态势标注：红圈 X 驱逐舰沉没 / 紫色爆炸云 港口高价值目标摧毁 */}
        <Source id="tactical-symbols" type="geojson" data={tacticalSymbolsGeoJson}>
          <Layer
            id="tactical-symbols-circle"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 4, 8, 8, 12, 12],
              'circle-color': ['match', ['get', 'symbol'], 'destruction', '#EF4444', 'explosion', '#A855F7', '#94A3B8'],
              'circle-stroke-width': 1.5,
              'circle-stroke-color': '#fff',
              'circle-opacity': 0.9,
            }}
          />
          <Layer
            id="tactical-symbols-icon"
            type="symbol"
            layout={{
              'text-field': ['match', ['get', 'symbol'], 'destruction', '×', 'explosion', '◆', ''],
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 10, 8, 14, 12, 18],
              'text-anchor': 'center',
              'text-allow-overlap': true,
            }}
            paint={{
              'text-color': '#fff',
              'text-halo-color': 'rgba(0,0,0,0.6)',
              'text-halo-width': 1,
            }}
          />
          <Layer
            id="tactical-symbols-label"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 8, 9, 10, 11],
              'text-anchor': 'top',
              'text-offset': [0, 0.6],
            }}
            paint={{
              'text-color': '#E5E7EB',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
        </Source>
        {/* 林肯号打击光点 */}
        <Source
          id="allied-strike-dots-lincoln"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: lincolnPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-lincoln-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(96, 165, 250, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-lincoln-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#3B82F6',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 福特号打击光点 */}
        <Source
          id="allied-strike-dots-ford"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: fordPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-ford-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(6, 182, 212, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-ford-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#06B6D4',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 以色列打击光点 */}
        <Source
          id="allied-strike-dots-israel"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: israelPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-israel-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(34, 211, 238, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-israel-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#22D3EE',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 斯里兰卡伏击：鱼雷轨迹光点（蓝色） */}
        <Source
          id="allied-strike-dots-virginia"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: effectiveVirginiaSrilankaPathsDisplay.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-virginia-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(59, 130, 246, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-virginia-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#3B82F6',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 阿巴斯港突袭：战斧弹道光点（紫色） */}
        <Source
          id="allied-strike-dots-tomahawk"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: effectiveTomahawkArabianPathsDisplay.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-tomahawk-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(168, 85, 247, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-tomahawk-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#A855F7',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 阿巴斯港同艇潜射/鱼雷光点（青色，与战斧同时） */}
        <Source
          id="allied-strike-dots-arabian-sub"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: effectiveArabianSubTorpedoPathsDisplay.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: { type: 'Point' as const, coordinates: path[0] },
            })),
          }}
        >
          <Layer
            id="allied-strike-dots-arabian-sub-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(6, 182, 212, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="allied-strike-dots-arabian-sub-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#06B6D4',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>
        {/* 盟军打击目标点位 (蓝色)：含林肯/福特/以色列→伊朗 + 以色列→黎巴嫩 + 斯里兰卡/阿巴斯港，统一名称与脉冲动效 */}
        <Source
          id="allied-strike-targets"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: alliedStrikeTargetsFeatures,
          }}
        >
          <Layer
            id="allied-strike-targets-circle"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 1.5, 5, 2.5, 8, 4],
              'circle-color': '#3B82F6',
              'circle-stroke-width': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 7, 1],
              'circle-stroke-color': '#fff',
            }}
          />
          <Layer
            id="allied-strike-targets-label"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
              'text-anchor': 'top',
              'text-offset': [0, 0.8],
            }}
            paint={{
              'text-color': '#FFFFFF',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
          <Layer
            id="allied-strike-targets-pulse"
            type="circle"
            paint={{
              'circle-radius': 0,
              'circle-color': 'rgba(96, 165, 250, 0.5)',
              'circle-opacity': 0,
            }}
          />
        </Source>

        {/* 伊朗攻击路径：细线 (矢量) */}
        <Source id="attack-lines" type="geojson" data={attackLinesGeoJson}>
          <Layer
            id="attack-lines"
            type="line"
            paint={{
              'line-color': 'rgba(255, 100, 100, 0.4)',
              'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.5, 8, 1, 12, 2],
            }}
          />
        </Source>
        {/* 光点飞行动画：从德黑兰飞向各目标 */}
        <Source
          id="attack-dots"
          type="geojson"
          data={{
            type: 'FeatureCollection',
            features: attackPaths.map((path) => ({
              type: 'Feature' as const,
              properties: {},
              geometry: {
                type: 'Point' as const,
                coordinates: path[0],
              },
            })),
          }}
        >
          <Layer
            id="attack-dots-glow"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 3, 8, 6, 12, 10],
              'circle-color': 'rgba(255, 100, 100, 0.6)',
              'circle-blur': 0.3,
            }}
          />
          <Layer
            id="attack-dots-core"
            type="circle"
            paint={{
              'circle-radius': ['interpolate', ['linear'], ['zoom'], 4, 1, 8, 2, 12, 4],
              'circle-color': '#ff4444',
              'circle-stroke-width': 0.5,
              'circle-stroke-color': '#fff',
            }}
          />
        </Source>

        {/* 中文标注 - 随 zoom 自适应大小 */}
        <Source id="labels" type="geojson" data={labelsGeoJson}>
          <Layer
            id="labels"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 3, 6, 5, 8, 7, 10, 10, 13],
              'text-anchor': 'top',
              'text-offset': [0, 0.8],
            }}
            paint={{
              'text-color': '#E5E7EB',
              'text-halo-width': 0,
            }}
          />
        </Source>

        {/* 胡塞武装失控区 - 伊朗红 */}
        <Source
          id="houthi-area"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '胡塞武装' },
            geometry: {
              type: 'Polygon',
              coordinates: [HOUTHI_POLYGON],
            },
          }}
        >
          <Layer
            id="houthi-fill"
            type="fill"
            paint={{
              'fill-color': 'rgba(239, 68, 68, 0.2)',
              'fill-outline-color': '#EF4444',
              'fill-opacity': 0.4,
            }}
          />
        </Source>
        {/* 胡塞武装标注 */}
        <Source
          id="houthi-label"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '胡塞武装' },
            geometry: { type: 'Point', coordinates: [44, 15.5] },
          }}
        >
          <Layer
            id="houthi-label-text"
            type="symbol"
            layout={{
              'text-field': '胡塞武装',
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 12],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#EF4444',
              'text-halo-width': 0,
            }}
          />
        </Source>

        {/* 苏丹：使用 Mapbox/countries 源绘制国境线（伊朗色标） */}
        <Source
          id="sudan-label"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '苏丹武装' },
            geometry: { type: 'Point', coordinates: EXTENDED_WAR_ZONES.sudanLabelCenter },
          }}
        >
          <Layer
            id="sudan-label-text"
            type="symbol"
            layout={{
              'text-field': '苏丹武装',
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 9, 8, 12],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#EF4444',
              'text-halo-width': 0,
            }}
          />
        </Source>

        {/* 伊朗标注 */}
        <Source
          id="iran-label"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '伊朗' },
            geometry: { type: 'Point', coordinates: [53.5, 32.5] },
          }}
        >
          <Layer
            id="iran-label-text"
            type="symbol"
            layout={{
              'text-field': '伊朗',
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#EF4444',
              'text-halo-width': 0,
            }}
          />
        </Source>
        {/* 以色列标注 */}
        <Source
          id="israel-label"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '以色列' },
            geometry: { type: 'Point', coordinates: [35, 31] },
          }}
        >
          <Layer
            id="israel-label-text"
            type="symbol"
            layout={{
              'text-field': '以色列',
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#60A5FA',
              'text-halo-width': 0,
            }}
          />
        </Source>
        {/* 黎巴嫩标注（以色列打击黎巴嫩目标时可见） */}
        <Source
          id="lebanon-label"
          type="geojson"
          data={{
            type: 'Feature',
            properties: { name: '黎巴嫩' },
            geometry: { type: 'Point', coordinates: [35.7, 33.7] },
          }}
        >
          <Layer
            id="lebanon-label-text"
            type="symbol"
            layout={{
              'text-field': '黎巴嫩',
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 11, 8, 16],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#94A3B8',
              'text-halo-width': 0,
            }}
          />
        </Source>

        <Source id="countries" type="geojson" data={COUNTRIES_GEOJSON}>
          {/* 伊朗区域填充 - 红色系 */}
          <Layer
            id="iran-fill"
            type="fill"
            filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
            paint={{
              'fill-color': 'rgba(239, 68, 68, 0.15)',
              'fill-outline-color': '#EF4444',
              'fill-opacity': 0.5,
            }}
          />
          <Layer
            id="iran-outline"
            type="line"
            filter={['==', ['get', 'ADMIN'], IRAN_ADMIN]}
            paint={{
              'line-color': '#EF4444',
              'line-width': 2,
              'line-opacity': 0.9,
            }}
          />
          {/* 以色列区域填充 - 蓝色系 */}
          <Layer
            id="israel-fill"
            type="fill"
            filter={['==', ['get', 'ADMIN'], 'Israel']}
            paint={{
              'fill-color': 'rgba(96, 165, 250, 0.25)',
              'fill-outline-color': '#60A5FA',
              'fill-opacity': 0.6,
            }}
          />
          <Layer
            id="allies-outline"
            type="line"
            filter={['in', ['get', 'ADMIN'], ['literal', ALLIES_ADMIN]]}
            paint={{
              'line-color': '#3B82F6',
              'line-width': 1.5,
              'line-opacity': 0.8,
            }}
          />
          {/* 苏丹区域填充 + 国境线（Mapbox/countries 源，伊朗色） */}
          <Layer
            id="sudan-fill"
            type="fill"
            filter={['==', ['get', 'ADMIN'], 'Sudan']}
            paint={{
              'fill-color': 'rgba(239, 68, 68, 0.2)',
              'fill-outline-color': 'transparent',
              'fill-opacity': 0.4,
            }}
          />
          <Layer
            id="sudan-outline"
            type="line"
            filter={['==', ['get', 'ADMIN'], 'Sudan']}
            paint={{
              'line-color': '#EF4444',
              'line-width': 2,
              'line-opacity': 1,
            }}
          />
        </Source>

        {/* 霍尔木兹海峡交战区 - 金黄色 mesh 区域 */}
        <Source key={`zone-hormuz-${zoneSourceKey}`} id="hormuz-combat-zone" type="geojson" data={hormuzZone}>
          <Layer
            id="hormuz-combat-fill"
            type="fill"
            paint={{
              'fill-color': (hormuzZone.properties as any).style.fillColor,
              'fill-opacity': (hormuzZone.properties as any).style.fillOpacity ?? 0.4,
            }}
          />
          <Layer
            id="hormuz-combat-outline"
            type="line"
            paint={{
              'line-color': '#FACC15',
              'line-width': 1.5,
              'line-dasharray': [1.5, 1.5],
            }}
          />
        </Source>

        {/* 真主党势力范围 - 绿色半透明区域 */}
        <Source key={`zone-hezbollah-${zoneSourceKey}`} id="hezbollah-zone" type="geojson" data={hezbollahZone}>
          <Layer
            id="hezbollah-fill"
            type="fill"
            paint={{
              'fill-color': (hezbollahZone.properties as any).color || '#32CD32',
              'fill-opacity': 0.28,
            }}
          />
          <Layer
            id="hezbollah-outline"
            type="line"
            paint={{
              'line-color': '#22C55E',
              'line-width': 1.2,
            }}
          />
        </Source>

        {/* 霍尔木兹海峡区域标注 */}
        <Source id="hormuz-label" type="geojson" data={hormuzLabelData}>
          <Layer
            id="hormuz-label-text"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              // 字体进一步调小，避免与该区域多重效果叠加后显得拥挤
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 5.5, 7, 7.5, 10, 9],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#FACC15',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 0.8,
            }}
          />
        </Source>

        {/* 真主党势力范围标注 */}
        <Source id="hezbollah-label" type="geojson" data={hezbollahLabelData}>
          <Layer
            id="hezbollah-label-text"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              // 字体尽量小一些，避免遮挡
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#22C55E',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
        </Source>

        {/* 库尔德武装势力范围标注（参照真主党紫色区域标记） */}
        <Source id="kurdish-label" type="geojson" data={kurdishLabelData}>
          <Layer
            id="kurdish-label-text"
            type="symbol"
            layout={{
              'text-field': ['get', 'name'],
              'text-size': ['interpolate', ['linear'], ['zoom'], 4, 7, 7, 9, 10, 11],
              'text-anchor': 'center',
            }}
            paint={{
              'text-color': '#E066FF',
              'text-halo-color': '#1a1a1a',
              'text-halo-width': 1,
            }}
          />
        </Source>
      </Map>
    </div>
  )
}
