<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { createNodeBlockingRule, deleteNode, deleteTextMessage, getMapReportById, getNodeInfoById, getPositions, getTelemetry, getTextMessages } from '../api'
import type { MapReport, NodeInfo, PositionRecord, TelemetryRecord, TextMessage } from '../types'
import ConfirmDeleteModal from './ConfirmDeleteModal.vue'
import NodeTrajectoryMap from './NodeTrajectoryMap.vue'

const props = defineProps<{
  nodeId: string
  isAdmin: boolean
}>()

const nodeInfo = ref<NodeInfo | null>(null)
const mapReport = ref<MapReport | null>(null)
const messages = ref<TextMessage[]>([])
const positions = ref<PositionRecord[]>([])
const telemetry = ref<TelemetryRecord[]>([])
const loading = ref(true)
const chatLoadingOlder = ref(false)
const chatHasMore = ref(true)
const error = ref('')
const chatPageSize = 20
const chatHistoryRef = ref<HTMLElement | null>(null)
type GroupedTextMessage = TextMessage & { mergedCount: number; mergedMessages: TextMessage[] }
type PendingDeleteAction =
  | { kind: 'delete-message'; message: GroupedTextMessage }
  | { kind: 'delete-and-block-node'; message: GroupedTextMessage; nodeId: string; nodeNum: number | null }

const menuMessage = ref<GroupedTextMessage | null>(null)
const menuX = ref(0)
const menuY = ref(0)
const pendingDeleteAction = ref<PendingDeleteAction | null>(null)

const nodeTitle = computed(() => {
  return nodeInfo.value?.long_name || nodeInfo.value?.short_name || mapReport.value?.long_name || mapReport.value?.short_name || props.nodeId
})

const mergedNode = computed(() => {
  return {
    node_num: nodeInfo.value?.node_num ?? mapReport.value?.node_num ?? null,
    long_name: nodeInfo.value?.long_name || mapReport.value?.long_name || null,
    short_name: nodeInfo.value?.short_name || mapReport.value?.short_name || null,
    hw_model: nodeInfo.value?.hw_model || mapReport.value?.hw_model || null,
    role: nodeInfo.value?.role || mapReport.value?.role || null,
    updated_at: nodeInfo.value?.updated_at || mapReport.value?.updated_at || null,
  }
})

const deleteModalTitle = computed(() => {
  if (pendingDeleteAction.value?.kind === 'delete-and-block-node') {
    return '确认删除并屏蔽节点'
  }
  return '确认删除消息'
})

const deleteModalMessage = computed(() => {
  const action = pendingDeleteAction.value
  if (!action) {
    return ''
  }
  const count = deleteMessageCount(action.message)
  if (action.kind === 'delete-and-block-node') {
    return count > 1
      ? `确定要删除这组已合并的 ${count} 条聊天消息并屏蔽该节点吗？请输入屏蔽原因。`
      : '确定要删除这条聊天消息并屏蔽该节点吗？请输入屏蔽原因。'
  }
  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')

const groupedMessages = computed<GroupedTextMessage[]>(() => {
  const groups = new Map<string, GroupedTextMessage>()
  for (const message of messages.value) {
    const key = `${message.packet_id ?? ''}\n${message.text ?? ''}`
    const group = groups.get(key)
    if (group) {
      group.mergedCount += 1
      group.mergedMessages.push(message)
    } else {
      groups.set(key, { ...message, mergedCount: 1, mergedMessages: [message] })
    }
  }
  return Array.from(groups.values())
})

function formatTime(value: string): string {
  return new Date(value).toLocaleString()
}

function metricEntries(value: string | null): Array<[string, unknown]> {
  if (!value) {
    return []
  }
  try {
    const parsed = JSON.parse(value) as Record<string, unknown>
    return Object.entries(parsed)
  } catch {
    return [['raw', value]]
  }
}

function metricLabel(key: string): string {
  const labels: Record<string, string> = {
    air_util_tx: '空口发送占用',
    battery_level: '电量',
    channel_utilization: '信道占用',
    uptime_seconds: '运行时长',
    voltage: '电压',
  }
  return labels[key] || key
}

function metricValue(key: string, value: unknown): string {
  if (typeof value !== 'number') {
    return String(value)
  }
  if (key === 'battery_level') {
    return `${value}%`
  }
  if (key === 'voltage') {
    return `${value.toFixed(2)} V`
  }
  if (key === 'air_util_tx' || key === 'channel_utilization') {
    return `${value.toFixed(2)}%`
  }
  if (key === 'uptime_seconds') {
    const hours = Math.floor(value / 3600)
    const minutes = Math.floor((value % 3600) / 60)
    const seconds = Math.floor(value % 60)
    return `${hours}h ${minutes}m ${seconds}s`
  }
  return Number.isInteger(value) ? String(value) : value.toFixed(2)
}

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: GroupedTextMessage): TextMessage[] {
  return Array.from(new Map(message.mergedMessages.map((item) => [item.id, item])).values())
}

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

async function optional<T>(request: Promise<T>): Promise<T | null> {
  try {
    return await request
  } catch {
    return null
  }
}

async function loadInitialMessages() {
  const response = await getTextMessages(chatPageSize, 0, props.nodeId)
  messages.value = toChronological(response.items)
  chatHasMore.value = response.items.length === chatPageSize
  await nextTick()
  const el = chatHistoryRef.value
  if (el) {
    el.scrollTop = el.scrollHeight
  }
}

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

  const el = chatHistoryRef.value
  const previousScrollHeight = el?.scrollHeight ?? 0
  const previousScrollTop = el?.scrollTop ?? 0
  chatLoadingOlder.value = true
  try {
    const response = await getTextMessages(chatPageSize, messages.value.length, props.nodeId)
    messages.value = mergeMessages(messages.value, toChronological(response.items))
    chatHasMore.value = response.items.length === chatPageSize
    await nextTick()
    if (el) {
      el.scrollTop = el.scrollHeight - previousScrollHeight + previousScrollTop
    }
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  } finally {
    chatLoadingOlder.value = false
  }
}

function closeMessageMenu() {
  menuMessage.value = null
}

function openMessageMenu(message: GroupedTextMessage, event: MouseEvent) {
  if (!props.isAdmin) {
    return
  }
  menuMessage.value = message
  menuX.value = event.clientX
  menuY.value = event.clientY
}

function deleteSelectedMessage() {
  if (!menuMessage.value) {
    return
  }
  pendingDeleteAction.value = { kind: 'delete-message', message: menuMessage.value }
  closeMessageMenu()
}

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

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: GroupedTextMessage) {
  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]}`)
  }
}

function deleteAndBlockSelectedMessageNode() {
  if (!menuMessage.value) {
    return
  }
  const message = menuMessage.value
  pendingDeleteAction.value = {
    kind: 'delete-and-block-node',
    message,
    nodeId: message.from_id || props.nodeId,
    nodeNum: message.from_num ?? mergedNode.value.node_num ?? null,
  }
  closeMessageMenu()
}

async function performDeleteAndBlockMessageNode(payload: { message: GroupedTextMessage; nodeId: string; nodeNum: number | null; reason: string }) {
  try {
    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
      }
    }
    if (payload.nodeId === props.nodeId) {
      nodeInfo.value = null
      mapReport.value = null
    }
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  }
}

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

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

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

function cancelDeleteModal() {
  pendingDeleteAction.value = null
}

function handleKeydown(event: KeyboardEvent) {
  if (event.key === 'Escape') {
    closeMessageMenu()
  }
}

function handleChatScroll() {
  closeMessageMenu()
  const el = chatHistoryRef.value
  if (!el || el.scrollTop > 8) {
    return
  }
  loadOlderMessages()
}

async function loadDetails() {
  loading.value = true
  error.value = ''
  try {
    const [nodeData, reportData, positionData, telemetryData] = await Promise.all([
      optional(getNodeInfoById(props.nodeId)),
      optional(getMapReportById(props.nodeId)),
      getPositions(500, 0, props.nodeId),
      getTelemetry(200, 0, props.nodeId),
    ])
    nodeInfo.value = nodeData
    mapReport.value = reportData
    positions.value = positionData.items
    telemetry.value = telemetryData.items
    await loadInitialMessages()
  } catch (err) {
    error.value = err instanceof Error ? err.message : String(err)
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  window.addEventListener('click', closeMessageMenu)
  window.addEventListener('keydown', handleKeydown)
  loadDetails()
})

onBeforeUnmount(() => {
  window.removeEventListener('click', closeMessageMenu)
  window.removeEventListener('keydown', handleKeydown)
})
</script>

<template>
  <section class="detail-page">
    <div class="panel">
      <div class="panel-header">
        <div>
          <p class="eyebrow">Node detail</p>
          <h2>{{ nodeTitle }}</h2>
        </div>
        <span class="badge">{{ nodeId }}</span>
      </div>
      <p v-if="error" class="error">{{ error }}</p>
      <div v-if="loading" class="empty">正在加载节点详情...</div>
      <div v-else class="detail-summary-grid">
        <div><span>Node ID</span><strong>{{ nodeId }}</strong></div>
        <div><span>Node Num</span><strong>{{ mergedNode.node_num ?? '-' }}</strong></div>
        <div><span>Long Name</span><strong>{{ mergedNode.long_name || '-' }}</strong></div>
        <div><span>Short Name</span><strong>{{ mergedNode.short_name || '-' }}</strong></div>
        <div><span>硬件</span><strong>{{ mergedNode.hw_model || '-' }}</strong></div>
        <div><span>角色</span><strong>{{ mergedNode.role || '-' }}</strong></div>
        <div><span>User ID</span><strong>{{ nodeInfo?.user_id || '-' }}</strong></div>
        <div><span>授权</span><strong>{{ nodeInfo?.is_licensed ?? '-' }}</strong></div>
        <div><span>固件版本</span><strong>{{ mapReport?.firmware_version || '-' }}</strong></div>
        <div><span>区域</span><strong>{{ mapReport?.region || '-' }}</strong></div>
        <div><span>调制预设</span><strong>{{ mapReport?.modem_preset || '-' }}</strong></div>
        <div><span>最新坐标</span><strong>{{ mapReport?.latitude ?? '-' }}, {{ mapReport?.longitude ?? '-' }}</strong></div>
        <div><span>海拔</span><strong>{{ mapReport?.altitude ?? '-' }}</strong></div>
        <div><span>位置精度</span><strong>{{ mapReport?.position_precision ?? '-' }}</strong></div>
        <div><span>在线节点</span><strong>{{ mapReport?.num_online_local_nodes ?? '-' }}</strong></div>
        <div><span>上报位置</span><strong>{{ mapReport?.has_opted_report_location ?? '-' }}</strong></div>
        <div><span>更新时间</span><strong>{{ mergedNode.updated_at ? formatTime(mergedNode.updated_at) : '-' }}</strong></div>
      </div>
    </div>

    <div class="panel">
      <div class="panel-header">
        <div>
          <p class="eyebrow">Public Key</p>
          <h2>节点公钥</h2>
        </div>
      </div>
      <pre class="public-key-block">{{ nodeInfo?.public_key || '-' }}</pre>
    </div>

    <div class="detail-section-grid">
      <div class="panel detail-chat-panel">
        <div class="panel-header">
          <div>
            <p class="eyebrow">Chat</p>
            <h2>历史聊天记录：{{ nodeTitle }}</h2>
          </div>
          <span class="badge">{{ groupedMessages.length }}</span>
        </div>
        <div ref="chatHistoryRef" class="detail-chat-history" @scroll.passive="handleChatScroll">
          <div v-if="chatLoadingOlder" class="chat-loading">正在加载更早消息...</div>
          <div v-else-if="!chatHasMore && messages.length > 0" class="chat-end">没有更多历史消息</div>
          <div v-if="messages.length === 0" class="empty">暂无聊天记录</div>
          <div
            v-for="message in groupedMessages"
            :key="message.id"
            class="detail-chat-item"
            @contextmenu.prevent.stop="openMessageMenu(message, $event)"
          >
            <span class="chat-meta">
              <strong>{{ formatTime(message.created_at) }}</strong>
              <small>{{ message.topic }}</small>
            </span>
            <span class="chat-text">
              {{ message.text || '[binary]' }}
              <span v-if="message.mergedCount > 1" class="message-merge-count">x{{ message.mergedCount }}</span>
            </span>
          </div>
        </div>
        <div
          v-if="menuMessage"
          class="context-menu"
          :style="{ left: `${menuX}px`, top: `${menuY}px` }"
          @click.stop
        >
          <button class="danger" type="button" @click="deleteSelectedMessage">删除</button>
          <button class="danger" type="button" @click="deleteAndBlockSelectedMessageNode">删除并屏蔽节点</button>
        </div>
      </div>

      <div class="panel">
        <div class="panel-header">
          <div>
            <p class="eyebrow">Trajectory</p>
            <h2>地图轨迹：{{ nodeTitle }}</h2>
          </div>
          <span class="badge">{{ positions.length }}</span>
        </div>
        <NodeTrajectoryMap :positions="positions" />
      </div>
    </div>

    <div class="panel">
      <div class="panel-header">
        <div>
          <p class="eyebrow">Telemetry</p>
          <h2>遥测数据：{{ nodeTitle }}</h2>
        </div>
        <span class="badge">{{ telemetry.length }}</span>
      </div>
      <div class="node-table-wrap">
        <table class="node-table">
          <thead>
            <tr>
              <th>时间</th>
              <th>类型</th>
              <th>指标</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="item in telemetry" :key="item.id">
              <td>{{ formatTime(item.created_at) }}</td>
              <td>{{ item.telemetry_type || '-' }}</td>
              <td>
                <div class="metrics-grid">
                  <div v-for="[key, value] in metricEntries(item.metrics_json)" :key="key" class="metric-chip">
                    <span>{{ metricLabel(key) }}</span>
                    <strong>{{ metricValue(key, value) }}</strong>
                  </div>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
        <div v-if="telemetry.length === 0" class="empty">暂无遥测数据</div>
      </div>
    </div>

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