<template>
  <div class="map-monitor-page">
    <el-row :gutter="12" class="kpi-row">
      <el-col :span="8">
        <el-statistic title="在线车辆" :value="onlineCars">
          <template #suffix><span class="muted">/ {{ cars.length }}</span></template>
        </el-statistic>
      </el-col>
      <el-col :span="8">
        <el-statistic title="运行中任务" :value="runningMissions" />
      </el-col>
      <el-col :span="8">
        <el-statistic title="排队任务" :value="queuedMissions" />
      </el-col>
    </el-row>

    <el-row :gutter="12" class="main-row">
      <el-col :span="16">
        <div class="canvas-with-alarms">
          <Workspace3D
            ref="workspaceRef"
            :host="vrHost"
            :scope="auth.scope ?? 'Platform'"
            :token="auth.token ?? ''"
            :read-only="!!readOnly"
            :embed-ui="true"
            @pick="onPick"
            @select="onSelect"
            @ready="onWorkspaceReady"
          />
          <FloatingAlarmStack :alarms="alarms" @locate="onAlarmLocate" @ack="onAlarmAck" />
          <WorkspaceCanvasToolbar />
        </div>
      </el-col>
      <el-col :span="8" class="side-col">
        <el-card shadow="never" class="side-card workbench-card">
          <template #header>
            <div class="workbench-header">
              <el-tabs v-model="workbenchTab" class="workbench-tabs">
                <el-tab-pane label="车辆监控台" name="vehicle" />
                <el-tab-pane label="任务列表" name="mission" />
              </el-tabs>
              <el-button link type="primary" size="small" :loading="refreshing" @click="refreshAll">
                刷新
              </el-button>
            </div>
          </template>
          <VehicleMonitorPanel
            v-show="workbenchTab === 'vehicle'"
            :cars="cars"
            :missions="missions"
            :selected-id="selectedVehicleId"
            @select="onVehicleSelect"
          />
          <MissionListPanel
            v-show="workbenchTab === 'mission'"
            :deliveries="deliveries"
            :selected-id="selectedDeliveryId"
            @select="onDeliverySelect"
            @refresh="onDeliveryListRefresh"
            @action-done="refreshDeliveries"
          />
        </el-card>
        <el-card shadow="never" class="side-card detail-card">
          <template #header><span>选中信息</span></template>
          <MonitorSelectionPanel
            :selection="selection"
            :refresh-key="tick"
            :cars="cars"
            :missions="missions"
            :read-only="!!readOnly"
          />
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import Workspace3D from '@/components/Workspace3D.vue'
import VehicleMonitorPanel from '@/components/workbench/VehicleMonitorPanel.vue'
import MissionListPanel from '@/components/workbench/MissionListPanel.vue'
import MonitorSelectionPanel from '@/components/workbench/MonitorSelectionPanel.vue'
import FloatingAlarmStack from '@/components/map-monitor/FloatingAlarmStack.vue'
import WorkspaceCanvasToolbar from '@/components/workspace/WorkspaceCanvasToolbar.vue'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { listCars, listMissions } from '@/api/projection'
import { listDeliveries } from '@/api/delivery'
import { useProjectionStream, type StreamEvent } from '@/composables/useProjectionStream'
import type { AlarmEvent, SelectionDetailEvent } from '@/composables/useMapEditStream'
import { reflectionApi } from '@/api/reflection'
import type { Car } from '@/types/car'
import type { Mission } from '@/types/mission'
import type { DeliveryTask } from '@/types/delivery'
import type { SelectedObjectRef } from '@/types/workbench'

defineProps<{
  /** 只读模式（运营端复用 MapMonitorView 时传 true）：3D 不可编辑，选中信息面板动作改用运维白名单。 */
  readOnly?: boolean
}>()

const auth = useAuthStore()
const vrHost = (import.meta.env.VITE_VRENDER_HOST as string | undefined) ?? 'localhost:8223'

const cars = ref<Car[]>([])
const missions = ref<Mission[]>([])
const deliveries = ref<DeliveryTask[]>([])
const deliveryListOpts = ref({ includeFinished: true, includeAborted: true })
const selection = ref<SelectedObjectRef | null>(null)
const tick = ref(0)
const refreshing = ref(false)
const workbenchTab = ref<'vehicle' | 'mission'>('vehicle')
const selectedDeliveryId = ref<number | null>(null)
const workspaceRef = ref<InstanceType<typeof Workspace3D> | null>(null)

/**
 * 浮动报警：订阅后端 AlarmStreamService 推送的 `alarm` 事件，
 * 维持一个滚动列表；超过 30 条丢最旧。clear 事件由 FloatingAlarmStack 自动过滤。
 *
 * 注意：本页只持有一个 useProjectionStream 实例，所有 SSE 事件（alarm / car-state /
 * mission-status / selection-detail / ...）共用同一条 EventSource，避免历史上「自己 + useMapEditStream
 * 各开一条」造成的两路 SSE 占用。
 */
const alarms = ref<AlarmEvent[]>([])

async function onAlarmLocate(a: AlarmEvent) {
  // 通过 reflectionApi.setSelection 把 SimpleLite 3D 场景的高亮切到该车，
  // CycleGUI 默认会把相机聚焦到 GetPosition 命中目标——视觉上等同于 flyTo。
  // a.carId 已经是 number（AlarmEvent 类型保证），无须额外字符串处理；如果将来后端
  // 改成字符串，这里 Number(...) 会返回 NaN 并由下面的 isFinite 拦下来。
  const numId = Number(a.carId)
  if (!Number.isFinite(numId)) {
    ElMessage.warning(`报警车辆 id 无法解析：${a.carId}`)
    return
  }
  try {
    await reflectionApi.setSelection('car', numId)
    selection.value = { kind: 'vehicle', id: String(numId), name: a.carName ?? String(a.carId) }
  } catch (err) {
    ElMessage.error(`定位失败：${(err as Error).message}`)
  }
}
function onAlarmAck(_a: AlarmEvent) {
  // 当前后端无 ACK 接口，前端本地标记即可（FloatingAlarmStack 已处理）
}

let pollTimer: ReturnType<typeof setInterval> | null = null

const onlineCars = computed(() => cars.value.filter((c) => c.state !== 'offline').length)
const runningMissions = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Fetching' || d.statusCode === 'Putting').length
    : missions.value.filter((m) => m.status === 'running').length
)
const queuedMissions = computed(() =>
  deliveries.value.length > 0
    ? deliveries.value.filter((d) => d.statusCode === 'Waiting').length
    : missions.value.filter((m) => m.status === 'queued').length
)

const selectedVehicleId = computed(() => {
  if (!selection.value || selection.value.kind !== 'vehicle') return null
  const sel = selection.value
  const match = cars.value.find((c) => c.id === sel.id || detailIdForCar(c) === sel.id)
  return match?.id ?? null
})

function onPick(_p: { x: number; y: number }) { /* 坐标由 3D 选中对象驱动 */ }

// SimpleLite 3D 端发送的对象命名规则（见 SimpleUI.cs / UISite.cs / TrackUiHelper）：
//   - 车辆：`Car-{id}` 或 `UICar-{id}`（不同入口不同前缀，正则同时兼容）
//   - 站点：`UISite-{id}`
//   - 路径：`UITrack-{id}` / `UICircularArcTrack-{id}` / `UIBezierTrack-{id}` / `UINurbsTrack-{id}`
const RX_CAR = /^(?:UI)?(?:Car|Vehicle)-(\d+)$/i
const RX_SITE = /^UISite-(\d+)$/i
const RX_TRACK = /^(?:UITrack|UICircularArcTrack|UIBezierTrack|UINurbsTrack)-(\d+)$/i

function parseSceneObjectName(name: string): { kind: 'vehicle' | 'site' | 'track'; id: string } | null {
  const car = RX_CAR.exec(name)
  if (car) return { kind: 'vehicle', id: car[1]! }
  const site = RX_SITE.exec(name)
  if (site) return { kind: 'site', id: site[1]! }
  const track = RX_TRACK.exec(name)
  if (track) return { kind: 'track', id: track[1]! }
  // 与地图编辑页一致：贪婪吃掉 UI 前缀字母段，兼容 UIBezierTrack-3 等
  const m = /^UI([A-Za-z]+)-(\d+)$/.exec(name) ?? /^([A-Za-z]+)-(\d+)$/.exec(name)
  if (!m) return null
  const tn = m[1]!.toLowerCase()
  const id = m[2]!
  if (tn === 'site') return { kind: 'site', id }
  if (tn === 'car' || tn === 'vehicle') return { kind: 'vehicle', id }
  if (tn.includes('track')) return { kind: 'track', id }
  return null
}

function sameSelection(a: SelectedObjectRef | null, b: SelectedObjectRef | null): boolean {
  if (!a && !b) return true
  if (!a || !b) return false
  return a.kind === b.kind && a.id === b.id
}

/** 把解析结果写入侧栏 selection；仅当对象变化时刷新详情，避免选中闪烁。 */
function applyParsedSelection(parsed: { kind: 'vehicle' | 'site' | 'track'; id: string }, rawName?: string) {
  selectedDeliveryId.value = null
  let next: SelectedObjectRef
  if (parsed.kind === 'vehicle') {
    const car = (rawName ? findSelectedCar(rawName) : undefined)
      ?? cars.value.find((c) => detailIdForCar(c) === parsed.id)
    next = car
      ? { kind: 'vehicle', id: detailIdForCar(car), name: car.name }
      : { kind: 'vehicle', id: parsed.id, name: `Vehicle ${parsed.id}` }
  } else {
    next = {
      kind: parsed.kind,
      id: parsed.id,
      name: rawName || `${parsed.kind === 'site' ? 'Site' : 'Track'} ${parsed.id}`
    }
  }
  if (sameSelection(selection.value, next)) return
  selection.value = next
  tick.value++
}

function applySelectionFromKindId(kind: 'site' | 'track' | 'car', id: number, names?: readonly string[]) {
  selectedDeliveryId.value = null
  if (kind === 'car') {
    const sid = String(id)
    const rawName = names?.find((n) => RX_CAR.test(n)) ?? `Car-${id}`
    const car = findSelectedCar(rawName) ?? cars.value.find((c) => detailIdForCar(c) === sid || c.id === sid)
    const next = car
      ? { kind: 'vehicle' as const, id: detailIdForCar(car), name: car.name }
      : { kind: 'vehicle' as const, id: sid, name: `Vehicle ${sid}` }
    if (!sameSelection(selection.value, next)) {
      selection.value = next
      tick.value++
    }
    return
  }
  const sid = String(id)
  const rawName = names?.find((n) => {
    const p = parseSceneObjectName(n)
    return p?.kind === kind && p.id === sid
  })
  const next = {
    kind,
    id: sid,
    name: rawName || `${kind === 'site' ? 'Site' : 'Track'} ${sid}`
  }
  if (!sameSelection(selection.value, next)) {
    selection.value = next
    tick.value++
  }
}

/**
 * SimpleLite 画布点选后通过 SSE `selection-detail` 广播（见 SimpleUI.BroadcastSelectionToWeb）。
 * embed 模式通常不会 postMessage `workspace.select`，因此必须在此同步侧栏。
 */
function onSelectionDetailFromStream(e: SelectionDetailEvent) {
  const siteN = e.siteIds?.length ?? 0
  const trackN = e.trackIds?.length ?? 0
  const carN = e.carIds?.length ?? 0
  const nameN = Array.isArray(e.names) ? e.names.length : 0
  if (siteN + trackN + carN + nameN === 0) {
    selection.value = null
    tick.value++
    return
  }

  if (Array.isArray(e.names)) {
    for (const n of e.names) {
      const parsed = parseSceneObjectName(n)
      if (parsed) {
        applyParsedSelection(parsed, n)
        return
      }
    }
  }

  if (e.kind && e.id) {
    applySelectionFromKindId(e.kind, e.id, e.names)
    return
  }

  if (siteN > 0) {
    applySelectionFromKindId('site', e.siteIds![0]!, e.names)
    return
  }
  if (trackN > 0) {
    applySelectionFromKindId('track', e.trackIds![0]!, e.names)
    return
  }
  if (carN > 0) {
    applySelectionFromKindId('car', e.carIds![0]!, e.names)
  }
}

function onSelect(names: string[]) {
  if (!names.length) {
    // embed 画布可能不发 select 或发空数组；勿清空，改读后端当前选中。
    void fallbackSelectionFromBackend()
    return
  }

  for (const n of names) {
    const parsed = parseSceneObjectName(n)
    if (!parsed) continue
    applyParsedSelection(parsed, n)
    return
  }

  void fallbackSelectionFromBackend()
}

function onWorkspaceReady() {
  void fallbackSelectionFromBackend()
}

async function fallbackSelectionFromBackend() {
  try {
    const sel = await reflectionApi.getSelection()
    if (!sel?.kind || sel.id == null) return
    if (sel.kind !== 'car' && sel.kind !== 'site' && sel.kind !== 'track') return
    applySelectionFromKindId(sel.kind, Number(sel.id), sel.name ? [sel.name] : undefined)
  } catch {
    // 仅作为命名兼容兜底，失败时保持当前 selection，不打断主流程。
  }
}

async function onVehicleSelect(ref: SelectedObjectRef) {
  selectedDeliveryId.value = null
  if (!sameSelection(selection.value, ref)) {
    selection.value = ref
    tick.value++
  }
  const id = Number(ref.id)
  if (!Number.isFinite(id)) return
  try {
    await reflectionApi.setSelection('car', id)
  } catch (err) {
    ElMessage.error(`同步 3D 选中失败：${(err as Error).message}`)
  }
}

async function onDeliverySelect(task: DeliveryTask) {
  selectedDeliveryId.value = task.id
  if (task.carId != null) {
    const car = cars.value.find((c) => c.rawId === task.carId || detailIdForCar(c) === String(task.carId))
    if (car) {
      await onVehicleSelect({ kind: 'vehicle', id: detailIdForCar(car), name: car.name })
      return
    }
    try {
      await reflectionApi.setSelection('car', task.carId)
      selection.value = {
        kind: 'vehicle',
        id: String(task.carId),
        name: task.carName ?? `Vehicle ${task.carId}`
      }
      tick.value++
    } catch (err) {
      ElMessage.error(`同步 3D 选中失败：${(err as Error).message}`)
    }
    return
  }
  selection.value = {
    kind: 'delivery',
    id: String(task.id),
    name: `${task.srcLabel} → ${task.dstLabel}`
  }
  tick.value++
}

function onDeliveryListRefresh(opts: { includeFinished: boolean; includeAborted: boolean }) {
  deliveryListOpts.value = opts
  void refreshDeliveries()
}

async function refreshDeliveries() {
  try {
    deliveries.value = await listDeliveries(deliveryListOpts.value)
  } catch (err) {
    console.warn('[MapMonitor] refreshDeliveries failed', err)
    deliveries.value = []
  }
}

function findSelectedCar(name: string): Car | undefined {
  const parsedId = parseWorkspaceCarId(name)
  return cars.value.find((c) =>
    c.name === name ||
    c.id === name ||
    (parsedId != null && detailIdForCar(c) === parsedId)
  )
}

function parseWorkspaceCarId(name: string): string | null {
  const m = /^(?:UI)?(?:Car|Vehicle)-(\d+)$/i.exec(name)
  return m?.[1] ?? null
}

function detailIdForCar(car: Car): string {
  if (car.rawId != null) return String(car.rawId)
  return car.id.replace(/^C/i, '').replace(/^0+/, '') || car.id
}

async function refreshAll() {
  refreshing.value = true
  try {
    const [carList, missionList] = await Promise.all([listCars(), listMissions()])
    cars.value = carList
    missions.value = missionList
    await refreshDeliveries()
  } catch (err) {
    console.warn('[MapMonitor] refreshAll failed', err)
  } finally {
    tick.value++
    refreshing.value = false
  }
}

// SSE 实时通道：本页只起一条流，所有事件都从这里分发。
//
// 刷新策略：
// - car-state / mission-status：后端真正的"车辆/任务状态变更"事件 → 立刻 refreshAll（目前后端
//   还没真正实现这两条，但为未来事件型推送预留）。
// - alarm：直接更新 alarms 列表，不需要 refreshAll。
// - snapshot-tick：后端 1Hz 心跳，仅含 cars/missions/sites/tracks 数字，不带实际状态。**不要**
//   在这里触发 refreshAll，否则会等价"1s 全量轮询 listCars+listMissions"，比原来的 3s 轮询还
//   重。snapshot-tick 仅用于 `stream.connected` 心跳判定。
// - selection-detail：画布点选后 SimpleLite 广播，驱动右侧「选中信息」面板（embed 模式主路径）。
// - 其它 object-* / pick-result / project-*：本页不处理，由地图编辑页订阅。
//
// 实时通道未连接（旧 SimpleLite / 离线）时退回 3s 轮询保底。
const stream = useProjectionStream({ autoConnect: true })

function onStreamEvent(e: StreamEvent) {
  if (!e?.kind) return
  switch (e.kind) {
    case 'alarm': {
      const a = e.payload as AlarmEvent | undefined
      if (!a) return
      // clear 事件 carId 同样有，但 info 为空；FloatingAlarmStack 内部按 info 是否为空过滤。
      alarms.value = [a, ...alarms.value.filter((x) => x.carId !== a.carId)].slice(0, 30)
      break
    }
    case 'car-state':
    case 'mission-status':
      void refreshAll()
      break
    case 'selection-detail':
      if (e.payload && typeof e.payload === 'object') {
        onSelectionDetailFromStream(e.payload as SelectionDetailEvent)
      }
      break
    case 'monitor-config-updated':
      // 运营维护保存后 SimpleLite 广播，刷新选中面板动作列表
      tick.value++
      break
    // 'snapshot-tick' 故意不触发 refreshAll —— 见上面注释。
  }
}
stream.on(onStreamEvent)

onMounted(async () => {
  await refreshAll()
  pollTimer = setInterval(() => {
    // SSE 已连但 projection/cars 曾 502 时 connected 仍为 true，需在车列表为空时继续轮询
    if (!stream.connected.value || cars.value.length === 0) {
      void refreshAll()
    } else if (workbenchTab.value === 'mission') {
      void refreshDeliveries()
    }
  }, 3000)
})

onUnmounted(() => {
  if (pollTimer) clearInterval(pollTimer)
  stream.off(onStreamEvent)
})
</script>

<style scoped>
.map-monitor-page {
  display: flex;
  flex-direction: column;
  gap: 14px;
  height: calc(100vh - 56px - 36px - 32px);
}
.kpi-row { flex: none; }
.main-row { flex: 1; min-height: 0; }
.main-row > .el-col { display: flex; flex-direction: column; min-height: 0; }
.side-col { gap: 12px; }
.side-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.workbench-card { flex: 1; min-height: 0; }
.detail-card { flex: 1; min-height: 0; }
.workbench-card :deep(.el-card__body),
.detail-card :deep(.el-card__body) {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.muted { color: var(--mg-text-muted); font-size: 12px; }
.workbench-card :deep(.el-card__header),
.detail-card :deep(.el-card__header) {
  color: var(--mg-text-light);
}
.workbench-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
}
.workbench-tabs {
  flex: 1;
  min-width: 0;
}
.workbench-tabs :deep(.el-tabs__header) {
  margin: 0;
}
.workbench-tabs :deep(.el-tabs__nav-wrap::after) {
  display: none;
}
.workbench-tabs :deep(.el-tabs__item) {
  color: var(--mg-text-muted);
  padding: 0 10px;
  height: 32px;
  line-height: 32px;
}
.workbench-tabs :deep(.el-tabs__item.is-active) {
  color: var(--mg-text-light);
}
.workbench-tabs :deep(.el-tabs__active-bar) {
  background-color: var(--mg-accent, #8ec8fc);
}
:deep(.el-row.main-row) { height: 100%; }
:deep(.el-col-16 > div) { height: 100%; min-height: 0; }
.canvas-with-alarms { position: relative; height: 100%; min-height: 0; flex: 1; display: flex; flex-direction: column; }
.canvas-with-alarms :deep(.workspace-3d-wrap) { flex: 1; }
</style>
