<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages, purgeNode } from './api'
import AdminBlockingManagement from './components/AdminBlockingManagement.vue'
import AdminBot from './components/AdminBot.vue'
import AdminBotDirect from './components/AdminBotDirect.vue'
import AdminDashboard from './components/AdminDashboard.vue'
import AdminLLM from './components/AdminLLM.vue'
import AdminLLMApi from './components/AdminLLMApi.vue'
import AdminDiscardDetails from './components/AdminDiscardDetails.vue'
import AdminHelpEdit from './components/AdminHelpEdit.vue'
import AdminLogin from './components/AdminLogin.vue'
import AdminLoginLogs from './components/AdminLoginLogs.vue'
import AdminMapSource from './components/AdminMapSource.vue'
import AdminMqttForward from './components/AdminMqttForward.vue'
import AdminSignManagement from './components/AdminSignManagement.vue'
import AdminUsers from './components/AdminUsers.vue'
import ChatPanel from './components/ChatPanel.vue'
import ConfirmDeleteModal from './components/ConfirmDeleteModal.vue'
import HelpPage from './components/HelpPage.vue'
import MeshMap from './components/MeshMap.vue'
import NodeDetailedPage from './components/NodeDetailedPage.vue'
import NodeListPanel from './components/NodeListPanel.vue'
import SignedPage from './components/SignedPage.vue'
import { fallbackMapSource, loadEnabledMapSources } from './mapSource'
import type { AdminUser, HealthStatus, MapBoundsChangePayload, MapBoundsQuery, MapRenderable, MapViewportItem, MapViewportPoint, NodeInfo, NodeInfoById, PositionRecord, PublicMapTileSource, TextMessage } from './types'

const currentPath = window.location.pathname
const adminPath = currentPath
const isAdminPage = adminPath.startsWith('/admin')
const isMqttForwardAdminPage = adminPath === '/admin/mqtt_forward' || adminPath === '/admin/mqtt_forward/'
const isBotAdminPage = adminPath === '/admin/bot' || adminPath === '/admin/bot/'
const isBotDirectAdminPage = adminPath === '/admin/bot/direct' || adminPath === '/admin/bot/direct/'
const isLLMAdminPage = adminPath === '/admin/llm' || adminPath === '/admin/llm/'
const isLLMApiAdminPage = adminPath === '/admin/llm/api' || adminPath === '/admin/llm/api/'
const isSignAdminPage = adminPath === '/admin/sign' || adminPath === '/admin/sign/'
const detailMatch = currentPath.match(/^\/detailed\/(.+)$/)
const detailedNodeId = detailMatch ? decodeURIComponent(detailMatch[1]) : ''
const isDetailedPage = !!detailedNodeId
const isHelpPage = currentPath === '/help'
const isSignedPage = currentPath === '/signed'
const adminUser = ref<AdminUser | null>(null)
const adminChecking = ref(false)

const loading = ref(true)
const nodePageLoading = ref(false)
const error = ref('')
const selectedNodeId = ref<string | null>(null)
const health = ref<HealthStatus | null>(null)
const nodeInfoSource = ref<NodeInfo[]>([])
const mapViewportItems = ref<MapViewportItem[]>([])
const selectedMapPoint = ref<MapViewportPoint | null>(null)
const mapViewportMode = ref<'points' | 'clusters'>('points')
const pagedNodeInfo = ref<NodeInfo[]>([])
const nodePage = ref(1)
const nodePageSize = 25
const nodeTotal = ref(0)
const messages = ref<TextMessage[]>([])
const chatPageSize = 20
const chatLoadingOlder = ref(false)
const chatHasMore = ref(true)
const chatInitialized = ref(false)
const positions = ref<PositionRecord[]>([])
const currentMapBounds = ref<MapBoundsQuery | null>(null)
const currentMapZoom = ref(2)
const mapReportsLoading = ref(false)
const mapReportTotal = ref(0)
const mapSources = ref<PublicMapTileSource[]>([fallbackMapSource])
const mapSource = ref<PublicMapTileSource>(fallbackMapSource)
const nodeFilter = ref('')
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)
type DeletableTextMessage = TextMessage & { mergedCount?: number; mergedMessages?: TextMessage[] }
type NodeActionRequest = { nodeId: string; nodeNum: number | null; message?: DeletableTextMessage }
type NodeActionPayload = NodeActionRequest & { reason: string }
type PendingDeleteAction =
  | { kind: 'delete-message'; message: DeletableTextMessage }
  | { kind: 'delete-node'; nodeId: string }
  | { kind: 'purge-node'; nodeId: string }
  | { kind: 'delete-displayed-nodes'; nodeIds: string[] }
  | ({ kind: 'delete-and-block-node' } & NodeActionRequest)
let refreshTimer: number | undefined
let mapBoundsTimer: number | undefined
let mapReportRequestSeq = 0

const nodesById = computed<NodeInfoById>(() => {
  const map = new Map<string, NodeInfo>()
  for (const node of nodeInfoSource.value) {
    map.set(node.node_id, node)
  }
  for (const node of pagedNodeInfo.value) {
    map.set(node.node_id, node)
  }
  return Object.fromEntries(map)
})

const normalizedNodeFilter = computed(() => nodeFilter.value.trim().toLowerCase())

function nodeMatchesFilterByInfo(node: NodeInfo | null | undefined, keyword: string): boolean {
  if (!keyword) {
    return true
  }
  if (!node) {
    return false
  }
  const fields = [node.node_id, node.long_name, node.short_name, node.hw_model, node.role]
  return fields.some((value) => (value ?? '').toString().toLowerCase().includes(keyword))
}

function nodeIdMatchesFilter(nodeId: string | null | undefined): boolean {
  const keyword = normalizedNodeFilter.value
  if (!keyword) {
    return true
  }
  if (!nodeId) {
    return false
  }
  if (nodeId.toLowerCase().includes(keyword)) {
    return true
  }
  return nodeMatchesFilterByInfo(nodesById.value[nodeId] ?? null, keyword)
}

function mapReportMatchesFilter(item: MapViewportPoint, keyword: string): boolean {
  if (!keyword) {
    return true
  }
  const fields = [item.node_id, item.long_name, item.short_name, item.hw_model, item.role]
  if (fields.some((value) => (value ?? '').toString().toLowerCase().includes(keyword))) {
    return true
  }
  return nodeMatchesFilterByInfo(nodesById.value[item.node_id] ?? null, keyword)
}

const filteredMessages = computed<TextMessage[]>(() => {
  if (!normalizedNodeFilter.value) {
    return messages.value
  }
  return messages.value.filter((message) => nodeIdMatchesFilter(message.from_id))
})

const filteredPagedNodeInfo = computed<NodeInfo[]>(() => {
  const keyword = normalizedNodeFilter.value
  if (!keyword) {
    return pagedNodeInfo.value
  }
  // 过滤启用时，跨整个 nodeInfoSource（最多 500 条）匹配，分页交给前端忽略
  return nodeInfoSource.value.filter((node) => nodeMatchesFilterByInfo(node, keyword))
})

const filteredNodeTotal = computed(() => {
  if (!normalizedNodeFilter.value) {
    return nodeTotal.value
  }
  return filteredPagedNodeInfo.value.length
})

const mapItems = computed<MapRenderable[]>(() => {
  const keyword = normalizedNodeFilter.value
  const items = keyword
    ? mapViewportItems.value.filter((item) => isMapViewportPoint(item) && mapReportMatchesFilter(item, keyword))
    : mapViewportItems.value
  const selectedItem = selectedMapPoint.value
  const renderItems = selectedItem && selectedItem.type === 'point' && (!keyword || mapReportMatchesFilter(selectedItem, keyword)) && !items.some((item) => item.type === 'point' && item.node_id === selectedItem.node_id)
    ? [...items, selectedItem]
    : items

  return renderItems
    .filter((item) => item.type === 'cluster' || (item.latitude != null && item.longitude != null))
    .map((item) => {
      if (item.type === 'cluster') {
        return {
          type: 'cluster',
          cluster_id: item.cluster_id,
          latitude: item.latitude,
          longitude: item.longitude,
          count: item.count,
        }
      }
      const nodeinfo = nodesById.value[item.node_id] ?? null
      return {
        type: 'node',
        node_id: item.node_id,
        label: item.short_name || item.long_name || nodeinfo?.short_name || nodeinfo?.long_name || item.node_id,
        latitude: item.latitude as number,
        longitude: item.longitude as number,
        altitude: item.altitude,
        source: 'map_report',
        updated_at: item.updated_at,
        nodeinfo,
        map_report: item,
        latest_position: null,
      }
    })
})

const deleteModalTitle = computed(() => {
  const action = pendingDeleteAction.value
  if (!action) {
    return ''
  }
  if (action.kind === 'delete-message') {
    return '确认删除消息'
  }
  if (action.kind === 'delete-node') {
    return '确认删除节点'
  }
  if (action.kind === 'purge-node') {
    return '确认删除节点'
  }
  if (action.kind === 'delete-displayed-nodes') {
    return '确认删除所有显示的节点'
  }
  return '确认删除并屏蔽节点'
})

const deleteModalMessage = computed(() => {
  const action = pendingDeleteAction.value
  if (!action) {
    return ''
  }
  if (action.kind === 'delete-message') {
    const count = deleteMessageCount(action.message)
    return count > 1
      ? `确定要删除这组已合并的 ${count} 条聊天消息吗？此操作不可撤销。`
      : '确定要删除这条聊天消息吗？此操作不可撤销。'
  }
  if (action.kind === 'delete-node') {
    return '确定要删除这个节点吗？此操作不可撤销。'
  }
  if (action.kind === 'purge-node') {
    return '确定要删除这个节点吗？将同时清理该节点的聊天消息和位置/遥测/路由/路径追踪等数据包记录，此操作不可撤销。'
  }
  if (action.kind === 'delete-displayed-nodes') {
    return `确定要删除当前地图上显示的全部 ${action.nodeIds.length} 个节点吗？将逐个调用删除接口，此操作不可撤销。`
  }
  if (!action.message) {
    return '确定要删除并屏蔽这个节点吗？请输入屏蔽原因。'
  }
  const count = deleteMessageCount(action.message)
  return count > 1
    ? `确定要删除这组已合并的 ${count} 条聊天消息并屏蔽该节点吗？请输入屏蔽原因。`
    : '确定要删除这条聊天消息并屏蔽该节点吗？请输入屏蔽原因。'
})

const deleteModalConfirmText = computed(() => {
  return pendingDeleteAction.value?.kind === 'delete-and-block-node' ? '删除并屏蔽' : '删除'
})

const deleteModalRequiresReason = computed(() => pendingDeleteAction.value?.kind === 'delete-and-block-node')

function toChronological(items: TextMessage[]): TextMessage[] {
  return [...items].reverse()
}

function compareMessages(a: TextMessage, b: TextMessage): number {
  const timeDiff = Date.parse(a.created_at) - Date.parse(b.created_at)
  return timeDiff !== 0 ? timeDiff : a.id - b.id
}

function mergeMessages(existing: TextMessage[], incoming: TextMessage[]): TextMessage[] {
  const byId = new Map<number, TextMessage>()
  for (const message of existing) {
    byId.set(message.id, message)
  }
  for (const message of incoming) {
    byId.set(message.id, message)
  }
  return Array.from(byId.values()).sort(compareMessages)
}

function messagesForDelete(message: DeletableTextMessage): TextMessage[] {
  const items = message.mergedMessages?.length ? message.mergedMessages : [message]
  return Array.from(new Map(items.map((item) => [item.id, item])).values())
}

function deleteMessageCount(message: DeletableTextMessage): number {
  return messagesForDelete(message).length
}

function isSameJSON(left: unknown, right: unknown): boolean {
  return JSON.stringify(left) === JSON.stringify(right)
}

function isMapViewportPoint(item: MapViewportItem): item is MapViewportPoint {
  return item.type === 'point'
}

function selectNode(nodeId: string) {
  selectedNodeId.value = nodeId
  const item = mapViewportItems.value.find((item): item is MapViewportPoint => isMapViewportPoint(item) && item.node_id === nodeId)
  selectedMapPoint.value = item ?? (selectedMapPoint.value?.node_id === nodeId ? selectedMapPoint.value : null)
}

function clearSelectedNode() {
  selectedNodeId.value = null
  selectedMapPoint.value = null
}

async function loadInitialChatMessages() {
  const response = await getTextMessages(chatPageSize, 0)
  messages.value = toChronological(response.items)
  chatHasMore.value = response.items.length === chatPageSize
  chatInitialized.value = true
}

async function loadOlderMessages() {
  if (chatLoadingOlder.value || !chatHasMore.value) {
    return
  }

  chatLoadingOlder.value = true
  try {
    const response = await getTextMessages(chatPageSize, messages.value.length)
    messages.value = mergeMessages(messages.value, toChronological(response.items))
    chatHasMore.value = response.items.length === chatPageSize
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  } finally {
    chatLoadingOlder.value = false
  }
}

async function pollLatestMessages() {
  const response = await getTextMessages(chatPageSize, 0)
  messages.value = mergeMessages(messages.value, toChronological(response.items))
}

async function loadNodePage(page: number, showLoading = true) {
  if (showLoading) {
    nodePageLoading.value = true
  }
  try {
    const safePage = Math.max(1, page)
    const response = await getNodeInfo(nodePageSize, (safePage - 1) * nodePageSize)
    pagedNodeInfo.value = response.items
    nodeTotal.value = response.total ?? response.offset + response.items.length
    nodePage.value = safePage
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  } finally {
    if (showLoading) {
      nodePageLoading.value = false
    }
  }
}

async function loadMapReportsForBounds(bounds: MapBoundsQuery, zoom: number, showLoading = true) {
  const requestSeq = ++mapReportRequestSeq
  if (showLoading) {
    mapReportsLoading.value = true
  }
  try {
    const response = await getMapReportViewport(bounds, zoom)
    if (requestSeq !== mapReportRequestSeq) {
      return
    }
    if (!isSameJSON(mapViewportItems.value, response.items)) {
      mapViewportItems.value = response.items
    }
    const selectedItem = selectedNodeId.value
      ? response.items.find((item): item is MapViewportPoint => isMapViewportPoint(item) && item.node_id === selectedNodeId.value)
      : null
    if (selectedItem) {
      selectedMapPoint.value = selectedItem
    }
    mapViewportMode.value = response.mode
    mapReportTotal.value = response.total
  } catch (err) {
    if (requestSeq === mapReportRequestSeq) {
      error.value = err instanceof Error ? err.message : String(err)
    }
  } finally {
    if (requestSeq === mapReportRequestSeq && showLoading) {
      mapReportsLoading.value = false
    }
  }
}

function handleMapBoundsChange(payload: MapBoundsChangePayload) {
  currentMapBounds.value = payload.bounds
  currentMapZoom.value = payload.zoom
  if (mapBoundsTimer !== undefined) {
    window.clearTimeout(mapBoundsTimer)
  }
  mapBoundsTimer = window.setTimeout(() => {
    loadMapReportsForBounds(payload.bounds, payload.zoom)
  }, 250)
}

async function refresh(showLoading = true) {
  if (showLoading) {
    loading.value = true
  }
  error.value = ''
  try {
    const [healthData, nodeInfoData, positionData] = await Promise.all([
      getHealth(),
      getNodeInfo(500, 0),
      getPositions(500),
    ])
    health.value = healthData
    nodeInfoSource.value = nodeInfoData.items
    positions.value = positionData.items
    await Promise.all([
      currentMapBounds.value ? loadMapReportsForBounds(currentMapBounds.value, currentMapZoom.value, false) : Promise.resolve(),
      loadNodePage(nodePage.value, showLoading),
      chatInitialized.value ? pollLatestMessages() : loadInitialChatMessages(),
    ])
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  } finally {
    if (showLoading) {
      loading.value = false
    }
  }
}

async function loadMapSource() {
  const sources = await loadEnabledMapSources()
  mapSources.value = sources
  mapSource.value = sources[0] ?? fallbackMapSource
}

function selectMapSource(sourceId: number) {
  const source = mapSources.value.find((item) => item.id === sourceId)
  if (source) {
    mapSource.value = source
  }
}

async function checkAdminSession() {
  adminChecking.value = true
  try {
    const response = await getAdminMe()
    adminUser.value = response.user
  } catch {
    adminUser.value = null
  } finally {
    adminChecking.value = false
  }
}

async function logoutAdmin() {
  try {
    await adminLogout()
  } finally {
    adminUser.value = null
  }
}

function requestDeleteMessage(message: DeletableTextMessage) {
  pendingDeleteAction.value = { kind: 'delete-message', message }
}

function requestDeleteNode(nodeId: string) {
  pendingDeleteAction.value = { kind: 'delete-node', nodeId }
}

function requestPurgeNode(nodeId: string) {
  pendingDeleteAction.value = { kind: 'purge-node', nodeId }
}

function requestDeleteDisplayedNodes() {
  // 收集当前地图上正在显示的节点 ID（仅 type=node，跳过聚合点），并按筛选后的视图顺序去重。
  const nodeIds = Array.from(
    new Set(
      mapItems.value
        .filter((item): item is Extract<MapRenderable, { type: 'node' }> => item.type === 'node')
        .map((item) => item.node_id),
    ),
  )
  if (nodeIds.length === 0) {
    error.value = '当前地图上没有可删除的节点。'
    return
  }
  pendingDeleteAction.value = { kind: 'delete-displayed-nodes', nodeIds }
}

function requestDeleteAndBlockNode(payload: NodeActionRequest) {
  pendingDeleteAction.value = { kind: 'delete-and-block-node', ...payload }
}

function cancelDeleteModal() {
  pendingDeleteAction.value = null
}

async function confirmDeleteModal(payload: { reason?: string }) {
  const action = pendingDeleteAction.value
  pendingDeleteAction.value = null
  if (!action) {
    return
  }

  if (action.kind === 'delete-message') {
    await deleteMessage(action.message)
    return
  }

  if (action.kind === 'delete-node') {
    await deleteNodeById(action.nodeId)
    return
  }

  if (action.kind === 'purge-node') {
    await purgeNodeById(action.nodeId)
    return
  }

  if (action.kind === 'delete-displayed-nodes') {
    await deleteDisplayedNodes(action.nodeIds)
    return
  }

  const reason = payload.reason?.trim()
  if (!reason) {
    return
  }
  await deleteAndBlockNode({
    nodeId: action.nodeId,
    nodeNum: action.nodeNum,
    message: action.message,
    reason,
  })
}

async function deleteMessage(message: DeletableTextMessage) {
  try {
    await deleteMessagesFromLocalState(message)
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  }
}

async function removeNodeFromLocalState(nodeId: string) {
  nodeInfoSource.value = nodeInfoSource.value.filter((node) => node.node_id !== nodeId)
  pagedNodeInfo.value = pagedNodeInfo.value.filter((node) => node.node_id !== nodeId)
  mapViewportItems.value = mapViewportItems.value.filter((item) => item.type === 'cluster' || item.node_id !== nodeId)
  if (selectedNodeId.value === nodeId) {
    clearSelectedNode()
  }
  await loadNodePage(nodePage.value, false)
}

function isAlreadyBlockedError(err: unknown): boolean {
  return err instanceof Error && err.message === 'blocking rule already exists'
}

function isNodeNotFoundError(err: unknown): boolean {
  return err instanceof Error && err.message === 'node not found'
}

function isMessageNotFoundError(err: unknown): boolean {
  return err instanceof Error && err.message === 'message not found'
}

async function deleteMessagesFromLocalState(message: DeletableTextMessage) {
  const items = messagesForDelete(message)
  const removableIds = new Set<number>()
  const errors: string[] = []

  await Promise.all(items.map(async (item) => {
    try {
      await deleteTextMessage(item.id)
      removableIds.add(item.id)
    } catch (err) {
      if (isMessageNotFoundError(err)) {
        removableIds.add(item.id)
        return
      }
      errors.push(err instanceof Error ? err.message : String(err))
    }
  }))

  if (removableIds.size > 0) {
    messages.value = messages.value.filter((item) => !removableIds.has(item.id))
  }
  if (errors.length > 0) {
    throw new Error(`部分消息删除失败（${errors.length}/${items.length}）：${errors[0]}`)
  }
}

async function deleteNodeById(nodeId: string) {
  try {
    await deleteNode(nodeId)
    await removeNodeFromLocalState(nodeId)
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  }
}

async function purgeNodeById(nodeId: string) {
  try {
    await purgeNode(nodeId)
    // 该节点的本地缓存包括聊天消息，一起从前端状态里清掉。
    messages.value = messages.value.filter((message) => message.from_id !== nodeId)
    await removeNodeFromLocalState(nodeId)
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  }
}

// 逐个调用 deleteNode 串行删除当前地图上显示的节点；某一个失败不阻断后续，最后汇总错误。
async function deleteDisplayedNodes(nodeIds: string[]) {
  const failures: string[] = []
  let lastErrorMessage = ''
  for (const nodeId of nodeIds) {
    try {
      await deleteNode(nodeId)
      await removeNodeFromLocalState(nodeId)
    } catch (err) {
      if (isNodeNotFoundError(err)) {
        // 已经不在了，本地状态也同步移除即可
        await removeNodeFromLocalState(nodeId)
        continue
      }
      failures.push(nodeId)
      lastErrorMessage = err instanceof Error ? err.message : String(err)
    }
  }
  if (failures.length > 0) {
    error.value = `部分节点删除失败（${failures.length}/${nodeIds.length}）：${lastErrorMessage}`
  }
}

async function deleteAndBlockNode(payload: NodeActionPayload) {
  try {
    if (payload.message) {
      await deleteMessagesFromLocalState(payload.message)
    }

    try {
      await createNodeBlockingRule({
        node_id: payload.nodeId,
        node_num: payload.nodeNum,
        reason: payload.reason,
        enabled: true,
      })
    } catch (err) {
      if (!isAlreadyBlockedError(err)) {
        throw err
      }
    }

    try {
      await deleteNode(payload.nodeId)
    } catch (err) {
      if (!isNodeNotFoundError(err)) {
        throw err
      }
    }
    await removeNodeFromLocalState(payload.nodeId)
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  }
}

onMounted(() => {
  if (isAdminPage) {
    checkAdminSession()
    return
  }
  checkAdminSession()
  if (isDetailedPage || isHelpPage || isSignedPage) {
    return
  }
  loadMapSource()
  refresh()
  refreshTimer = window.setInterval(() => refresh(false), 5000)
})

onBeforeUnmount(() => {
  if (refreshTimer !== undefined) {
    window.clearInterval(refreshTimer)
  }
  if (mapBoundsTimer !== undefined) {
    window.clearTimeout(mapBoundsTimer)
  }
})
</script>

<template>
  <main class="app-shell">
    <header class="topbar">
      <div>
        <p class="eyebrow">Meshtastic MQTT Server</p>
        <h1 v-if="isDetailedPage">节点详情</h1>
        <h1 v-else-if="isHelpPage">使用帮助</h1>
        <h1 v-else-if="isSignedPage">签到列表</h1>
        <h1 v-else>{{ isAdminPage ? 'Admin' : 'MeshMap' }}</h1>
      </div>
      <div class="topbar-actions">
        <template v-if="isAdminPage">
          <nav v-if="adminUser" class="admin-nav">
            <a href="/admin" :class="{ active: adminPath === '/admin' }">服务状态</a>
            <a href="/admin/users" :class="{ active: adminPath === '/admin/users' }">用户管理</a>
            <a href="/admin/blocking_management" :class="{ active: adminPath === '/admin/blocking_management' }">屏蔽管理</a>
            <a href="/admin/mqtt_forward/" :class="{ active: isMqttForwardAdminPage }">MQTT转发</a>
            <a href="/admin/bot" :class="{ active: isBotAdminPage }">机器人</a>
            <a href="/admin/bot/direct" :class="{ active: isBotDirectAdminPage }">机器人私聊</a>
            <a href="/admin/llm" :class="{ active: isLLMAdminPage }">LLM消息队列</a>
            <a href="/admin/llm/api" :class="{ active: isLLMApiAdminPage }">LLM API配置</a>
            <a href="/admin/sign" :class="{ active: isSignAdminPage }">签到管理</a>
            <a href="/admin/map_source" :class="{ active: adminPath === '/admin/map_source' }">地图图源</a>
            <a href="/admin/help_edit" :class="{ active: adminPath === '/admin/help_edit' }">帮助编辑</a>
            <a href="/admin/log/login" :class="{ active: adminPath === '/admin/log/login' }">登录日志</a>
            <a href="/admin/discard_details" :class="{ active: adminPath === '/admin/discard_details' }">丢弃数据</a>
          </nav>
          <a class="topbar-link" href="/">返回地图</a>
        </template>
        <template v-else-if="isDetailedPage">
          <span class="counter">{{ detailedNodeId }}</span>
          <a class="topbar-link" href="/">返回地图</a>
          <a class="topbar-link" href="/admin">管理</a>
        </template>
        <template v-else-if="isHelpPage">
          <a class="topbar-link" href="/">返回地图</a>
          <a class="topbar-link" href="/admin">管理</a>
        </template>
        <template v-else-if="isSignedPage">
          <a class="topbar-link" href="/">返回地图</a>
          <a class="topbar-link" href="/admin">管理</a>
        </template>
        <template v-else>

          
          <span class="counter">节点 {{ normalizedNodeFilter ? `${filteredNodeTotal} / ${nodeTotal}` : nodeTotal }} · 已加载消息 {{ normalizedNodeFilter ? `${filteredMessages.length} / ${messages.length}` : messages.length }} · 坐标 {{ mapItems.length }} / {{ mapReportTotal }}{{ mapViewportMode === 'clusters' ? ' · 已聚合' : '' }}{{ mapReportsLoading ? ' · 坐标加载中...' : '' }}{{ normalizedNodeFilter ? ' · 已筛选' : '' }}</span>
          <div class="topbar-filter">
            <input
              type="search"
              class="topbar-filter-input"
              v-model="nodeFilter"
              placeholder="筛选节点"
            />
            <button
              v-if="normalizedNodeFilter"
              type="button"
              class="topbar-filter-clear"
              @click="nodeFilter = ''"
            >清除</button>
          </div>
          <a class="topbar-link" href="/signed">签到列表</a>
          <a class="topbar-link" href="/help">使用帮助</a>
          <a class="topbar-link" href="/admin">管理</a>
          <!-- <button @click="() => refresh()" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
           -->
        </template>
      </div>
    </header>

    <template v-if="isAdminPage">
      <div v-if="adminChecking" class="panel admin-loading">正在检查登录状态...</div>
      <template v-else-if="adminUser">
        <div class="panel admin-session-card">
          <div>
            <p class="eyebrow">Session</p>
            <h2>当前登录：{{ adminUser.username }}</h2>
          </div>
          <button class="admin-button" @click="logoutAdmin">退出登录</button>
        </div>
        <AdminUsers v-if="adminPath === '/admin/users'" :user="adminUser" />
        <AdminBlockingManagement v-else-if="adminPath === '/admin/blocking_management'" />
        <AdminMqttForward v-else-if="isMqttForwardAdminPage" />
        <AdminBot v-else-if="isBotAdminPage" />
        <AdminBotDirect v-else-if="isBotDirectAdminPage" />
        <AdminLLM v-else-if="isLLMAdminPage" />
        <AdminLLMApi v-else-if="isLLMApiAdminPage" />
        <AdminSignManagement v-else-if="isSignAdminPage" />
        <AdminMapSource v-else-if="adminPath === '/admin/map_source'" />
        <AdminHelpEdit v-else-if="adminPath === '/admin/help_edit'" />
        <AdminLoginLogs v-else-if="adminPath === '/admin/log/login'" />
        <AdminDiscardDetails v-else-if="adminPath === '/admin/discard_details'" />
        <AdminDashboard v-else />
      </template>
      <AdminLogin v-else @login="adminUser = $event" />
    </template>

    <template v-else-if="isDetailedPage">
      <NodeDetailedPage :node-id="detailedNodeId" :is-admin="!!adminUser" />
    </template>

    <template v-else-if="isHelpPage">
      <HelpPage />
    </template>

    <template v-else-if="isSignedPage">
      <SignedPage />
    </template>

    <template v-else>
      <p v-if="error" class="error">{{ error }}</p>

      <section class="workspace">
        <ChatPanel
          :messages="filteredMessages"
          :nodes-by-id="nodesById"
          :selected-node-id="selectedNodeId"
          :loading-older="chatLoadingOlder"
          :has-more-messages="chatHasMore"
          :is-admin="!!adminUser"
          @select-node="selectNode"
          @load-older="loadOlderMessages"
          @delete-message="requestDeleteMessage"
          @delete-and-block-node="requestDeleteAndBlockNode"
        />
        <MeshMap
          :items="mapItems"
          :selected-node-id="selectedNodeId"
          :is-admin="!!adminUser"
          :auto-fit="false"
          :loading="mapReportsLoading"
          :map-source="mapSource"
          :map-sources="mapSources"
          @map-source-change="selectMapSource"
          @bounds-change="handleMapBoundsChange"
          @select-node="selectNode"
          @clear-node="clearSelectedNode"
          @delete-node="requestDeleteNode"
          @purge-node="requestPurgeNode"
          @delete-displayed-nodes="requestDeleteDisplayedNodes"
          @delete-and-block-node="requestDeleteAndBlockNode"
        />
      </section>

      <NodeListPanel
        :nodes="filteredPagedNodeInfo"
        :selected-node-id="selectedNodeId"
        :page="nodePage"
        :page-size="nodePageSize"
        :total="filteredNodeTotal"
        :loading="nodePageLoading || loading"
        :is-admin="!!adminUser"
        :filter-active="!!normalizedNodeFilter"
        @select-node="selectNode"
        @page-change="loadNodePage"
        @delete-node="requestDeleteNode"
        @purge-node="requestPurgeNode"
        @delete-and-block-node="requestDeleteAndBlockNode"
      />
    </template>

    <ConfirmDeleteModal
      :open="!!pendingDeleteAction"
      :title="deleteModalTitle"
      :message="deleteModalMessage"
      :confirm-text="deleteModalConfirmText"
      :require-reason="deleteModalRequiresReason"
      reason-label="屏蔽原因"
      reason-placeholder="请输入屏蔽原因"
      @cancel="cancelDeleteModal"
      @confirm="confirmDeleteModal"
    />
  </main>
</template>
