<template>
  <view class="page">
    <wd-navbar
      title="蓝牙调试"
      left-arrow
      placeholder
      safe-area-inset-top
      fixed
      @click-left="uni.navigateBack()"
    />

    <!-- ===== 步骤指示 ===== -->
    <view class="section">
      <view class="steps">
        <view
          v-for="(s, i) in stepList"
          :key="i"
          class="step"
          :class="{ 'step--active': step === i, 'step--done': step > i }"
          @click="gotoStep(i)"
        >
          <view class="step-num">
            {{ step > i ? '✓' : i + 1 }}
          </view>
          <text class="step-label">{{ s }}</text>
        </view>
      </view>
    </view>

    <!-- ===================== Step 0: BLE 通用扫描 ===================== -->
    <template v-if="step === 0">
      <!-- iOS 提示 -->
      <view v-if="isIOS" class="section">
        <view class="ios-tip">
          <view class="ios-tip-title">
            iOS 限制说明
          </view>
          <view class="ios-tip-desc">
            苹果系统会屏蔽 BLE 扫描中的 iBeacon 广播包，因此 BLE 扫描无法发现 iBeacon 设备。
          </view>
          <view class="ios-tip-desc" style="margin-top: 8rpx;">
            请直接跳到第 2 步，手动输入信标的 iBeacon UUID 进行监听。
            可先用安卓手机扫描获取 UUID。
          </view>
          <view class="action-btn action-btn--primary mt-16" @click="gotoStep(1)">
            跳到「识别信标」输入 UUID →
          </view>
        </view>
      </view>

      <!-- 控制 -->
      <view class="section">
        <view class="card control-card">
          <view class="control-main">
            <view class="title">
              BLE 通用扫描
            </view>
            <view class="desc">
              扫描所有 BLE 设备{{ isIOS ? '' : '，自动解析 iBeacon 广播帧' }}
            </view>
            <view class="desc">
              设备 {{ devices.length }} 个{{ isIOS ? '' : `，其中 iBeacon ${ibeaconDevices.length} 个` }}
            </view>
          </view>
          <wd-button
            :type="scanning ? 'error' : 'primary'"
            size="small"
            round
            @click="scanning ? stopBleScan() : startBleScan()"
          >
            {{ scanning ? '停止' : '开始' }}
          </wd-button>
        </view>
      </view>

      <view v-if="errorMsg" class="section">
        <view class="error-box">
          {{ errorMsg }}
        </view>
      </view>

      <!-- 筛选工具 -->
      <view v-if="devices.length" class="section tools">
        <view class="chip" :class="{ 'chip--active': bleFilter === 'all' }" @click="bleFilter = 'all'">
          全部 ({{ devices.length }})
        </view>
        <view class="chip" :class="{ 'chip--active': bleFilter === 'ibeacon' }" @click="bleFilter = 'ibeacon'">
          iBeacon ({{ ibeaconDevices.length }})
        </view>
        <view class="chip" :class="{ 'chip--active': bleFilter === 'named' }" @click="bleFilter = 'named'">
          有名称
        </view>
        <view class="chip" @click="devices = []">
          清空
        </view>
      </view>

      <!-- MAC 搜索 -->
      <view v-if="devices.length" class="section">
        <view class="input-wrap">
          <input
            v-model="targetMac"
            class="input"
            placeholder="搜索 MAC / 设备名 / 广播数据"
            confirm-type="done"
          >
          <view v-if="targetMac" class="clear" @click="targetMac = ''">
            清除
          </view>
        </view>
      </view>

      <!-- 设备列表 -->
      <view class="list-section">
        <view v-if="filteredBleDevices.length === 0" class="empty">
          <view class="empty-title">
            {{ scanning ? '正在搜索 BLE 设备...' : '点击「开始」扫描附近设备' }}
          </view>
          <view v-if="!scanning" class="empty-desc">
            会自动解析 iBeacon 广播帧，提取 UUID / Major / Minor
          </view>
        </view>

        <view
          v-for="device in filteredBleDevices"
          :key="device.deviceId"
          class="device-card"
          :class="{ 'device-card--beacon': !!device.ibeacon }"
          @click="handleDeviceTap(device)"
        >
          <view class="device-header">
            <view class="device-name">
              {{ device.name || device.localName || '未知设备' }}
              <text v-if="device.ibeacon" class="beacon-tag">iBeacon</text>
            </view>
            <view class="rssi">
              {{ device.RSSI }} dBm
            </view>
          </view>

          <view class="info-row">
            <text class="info-label">设备 ID</text>
            <text class="info-value mono break">{{ device.deviceId }}</text>
          </view>
          <view v-if="device.mac" class="info-row">
            <text class="info-label">解析 MAC</text>
            <text class="info-value mono break">{{ device.mac }}</text>
          </view>
          <view v-if="device.ibeacon" class="info-row">
            <text class="info-label">UUID</text>
            <text class="info-value mono break text-xs">{{ device.ibeacon.uuid }}</text>
          </view>
          <view v-if="device.ibeacon" class="info-row">
            <text class="info-label">Major/Minor</text>
            <text class="info-value mono">{{ device.ibeacon.major }} / {{ device.ibeacon.minor }}</text>
          </view>
          <view v-if="device.advertisData" class="info-row">
            <text class="info-label">广播数据</text>
            <text class="info-value mono break text-xs">{{ device.advertisData }}</text>
          </view>

          <!-- 操作按钮 -->
          <view class="device-actions">
            <view v-if="device.ibeacon" class="action-btn action-btn--primary" @click.stop="useAsBeacon(device)">
              用此 UUID 进入 iBeacon 监听 →
            </view>
            <view class="action-btn" @click.stop="connectAndRead(device)">
              {{ connectingId === device.deviceId ? '连接中...' : '连接读取设备信息' }}
            </view>
          </view>

          <!-- 连接后读到的服务/特征值 -->
          <view v-if="deviceServices[device.deviceId]" class="services-panel">
            <view class="services-title">
              设备信息（0x180A）
            </view>
            <view
              v-for="char in deviceServices[device.deviceId]"
              :key="char.name"
              class="info-row"
            >
              <text class="info-label">{{ char.name }}</text>
              <text class="info-value mono break">{{ char.value }}</text>
            </view>
            <view v-if="deviceServices[device.deviceId].length === 0" class="hint">
              该设备不支持 Device Information Service
            </view>
          </view>
        </view>
      </view>
    </template>

    <!-- ===================== Step 1: 识别确认 ===================== -->
    <template v-if="step === 1">
      <view class="section">
        <view class="card">
          <view class="title">
            确认目标信标
          </view>
          <view class="desc mt-12">
            {{ isIOS
              ? 'iOS 无法通过 BLE 扫描发现 iBeacon，请在下方手动输入 UUID'
              : '以下是从 BLE 扫描中发现的 iBeacon 信标，选择你的目标信标'
            }}
          </view>
        </view>
      </view>

      <!-- 手动输入 UUID（iOS 优先显示） -->
      <view v-if="isIOS" class="section">
        <view class="card">
          <view class="label">
            输入 iBeacon UUID
          </view>
          <view class="input-wrap">
            <input
              v-model="manualUuid"
              class="input"
              placeholder="从信标配置工具或安卓手机获取"
              confirm-type="done"
            >
          </view>
          <view class="hint">
            UUID 通常为 36 位格式，如 FDA50693-A4E2-4FB1-AFCB-194215961234
          </view>
          <view v-if="manualUuid" class="mt-16">
            <view class="action-btn action-btn--primary action-btn--block" @click="manualConfirmAndGo">
              使用此 UUID 进入 iBeacon 监听 →
            </view>
          </view>
        </view>
      </view>

      <view v-if="ibeaconDevices.length === 0 && !isIOS" class="section">
        <view class="empty">
          <view class="empty-title">
            未发现 iBeacon 设备
          </view>
          <view class="empty-desc">
            请返回第一步先进行 BLE 扫描
          </view>
          <view class="action-btn action-btn--primary mt-24" @click="gotoStep(0)">
            返回扫描
          </view>
        </view>
      </view>

      <view class="list-section">
        <view
          v-for="device in ibeaconDevices"
          :key="device.deviceId"
          class="device-card device-card--selectable"
          :class="{ 'device-card--selected': selectedDevice?.deviceId === device.deviceId }"
          @click="selectedDevice = device"
        >
          <view class="device-header">
            <view class="device-name">
              {{ device.name || '未知设备' }}
              <text class="beacon-tag">iBeacon</text>
            </view>
            <view class="rssi">
              {{ device.RSSI }} dBm
            </view>
          </view>
          <view class="info-row">
            <text class="info-label">UUID</text>
            <text class="info-value mono break text-xs">{{ device.ibeacon!.uuid }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">Major/Minor</text>
            <text class="info-value mono">{{ device.ibeacon!.major }} / {{ device.ibeacon!.minor }}</text>
          </view>
          <view v-if="device.mac" class="info-row">
            <text class="info-label">解析 MAC</text>
            <text class="info-value mono">{{ device.mac }}</text>
          </view>
        </view>
      </view>

      <view v-if="selectedDevice" class="section">
        <view class="action-btn action-btn--primary action-btn--block" @click="confirmAndGo">
          确认，进入 iBeacon 精确监听 →
        </view>
      </view>

      <!-- 手动输入 UUID（非 iOS） -->
      <view v-if="!isIOS" class="section">
        <view class="card">
          <view class="label">
            或手动输入 iBeacon UUID
          </view>
          <view class="input-wrap">
            <input
              v-model="manualUuid"
              class="input"
              placeholder="输入 iBeacon UUID"
              confirm-type="done"
            >
          </view>
          <view v-if="manualUuid" class="mt-16">
            <view class="action-btn action-btn--primary" @click="manualConfirmAndGo">
              使用此 UUID 进入监听 →
            </view>
          </view>
        </view>
      </view>
    </template>

    <!-- ===================== Step 2: iBeacon 精确监听 ===================== -->
    <template v-if="step === 2">
      <!-- 目标信息 -->
      <view class="section">
        <view class="card highlight-card">
          <view class="title">
            目标 iBeacon
          </view>
          <view class="info-row mt-12">
            <text class="info-label">UUID</text>
            <text class="info-value mono break text-xs">{{ activeBeaconUuid }}</text>
          </view>
          <view v-if="selectedDevice" class="info-row">
            <text class="info-label">设备名</text>
            <text class="info-value">{{ selectedDevice.name || '未知' }}</text>
          </view>
        </view>
      </view>

      <!-- 控制 -->
      <view class="section">
        <view class="card control-card">
          <view class="control-main">
            <view class="title">
              iBeacon 监听
            </view>
            <view class="desc">
              信标 {{ beacons.length }} 个
              <text v-if="scanning" class="scanning-dot" />
            </view>
          </view>
          <wd-button
            :type="scanning ? 'error' : 'primary'"
            size="small"
            round
            @click="scanning ? stopBeaconScan() : startBeaconScan()"
          >
            {{ scanning ? '停止' : '开始' }}
          </wd-button>
        </view>
      </view>

      <view v-if="errorMsg" class="section">
        <view class="error-box">
          {{ errorMsg }}
        </view>
      </view>

      <!-- 信标列表 -->
      <view class="list-section">
        <view v-if="sortedBeacons.length === 0" class="empty">
          <view class="empty-title">
            {{ scanning ? '正在监听 iBeacon...' : '点击「开始」监听信标' }}
          </view>
        </view>

        <view
          v-for="b in sortedBeacons"
          :key="b.key"
          class="device-card device-card--beacon"
        >
          <view class="device-header">
            <view class="device-name">
              iBeacon
              <text
                class="proximity-tag"
                :class="{
                  'proximity-tag--near': b.proximity === 1,
                  'proximity-tag--mid': b.proximity === 2,
                  'proximity-tag--far': b.proximity === 3,
                }"
              >
                {{ proximityText(b.proximity) }}
              </text>
            </view>
            <view class="rssi">
              {{ b.rssi }} dBm
            </view>
          </view>
          <view class="info-row">
            <text class="info-label">UUID</text>
            <text class="info-value mono break text-xs">{{ b.uuid }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">Major</text>
            <text class="info-value mono">{{ b.major }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">Minor</text>
            <text class="info-value mono">{{ b.minor }}</text>
          </view>
          <view class="info-row">
            <text class="info-label">精度</text>
            <text class="info-value">{{ b.accuracy.toFixed(2) }} m</text>
          </view>
          <view class="info-row">
            <text class="info-label">RSSI</text>
            <view class="rssi-bar-wrap">
              <view class="rssi-bar" :style="{ width: `${rssiPercent(b.rssi)}%` }" />
            </view>
            <text class="info-value mono" style="width: auto;">{{ b.rssi }}</text>
          </view>
        </view>
      </view>
    </template>

    <!-- ===== 日志面板 ===== -->
    <view v-if="logs.length" class="log-panel">
      <view class="log-head">
        <text class="log-title">日志 ({{ logs.length }})</text>
        <view class="log-actions">
          <text class="log-action" @click="showLog = !showLog">{{ showLog ? '收起' : '展开' }}</text>
          <text class="log-action" @click="logs = []">清空</text>
        </view>
      </view>
      <view v-if="showLog" class="log-body">
        <view
          v-for="(log, i) in logs"
          :key="i"
          class="log-item"
          :class="{
            'log-item--error': log.type === 'error',
            'log-item--success': log.type === 'success',
          }"
        >
          [{{ log.time }}] {{ log.msg }}
        </view>
      </view>
    </view>
  </view>
</template>

<script setup lang="ts">
import { computed, onUnmounted, reactive, ref } from 'vue'

definePage({
  style: {
    navigationBarTitleText: '',
    navigationStyle: 'custom',
  },
})

// ==================== 类型 ====================

interface IBeaconParsed {
  uuid: string
  major: number
  minor: number
}

interface BleDevice {
  name: string
  localName?: string
  deviceId: string
  mac?: string
  RSSI: number
  advertisData?: string
  ibeacon?: IBeaconParsed
}

interface BeaconItem {
  key: string
  uuid: string
  major: number
  minor: number
  rssi: number
  accuracy: number
  proximity: number
}

interface LogItem {
  time: string
  msg: string
  type: 'info' | 'error' | 'success'
}

interface CharInfo {
  name: string
  value: string
}

// ==================== 平台检测 ====================

const systemInfo = uni.getSystemInfoSync()
const isIOS = systemInfo.platform === 'ios'

// ==================== 状态 ====================

const stepList = ['BLE 扫描', '识别信标', 'iBeacon 监听']
const step = ref(isIOS ? 1 : 0) // iOS 默认跳到第 2 步

const scanning = ref(false)
const errorMsg = ref('')
const logs = ref<LogItem[]>([])
const showLog = ref(false)

// Step 0: BLE 扫描
const devices = ref<BleDevice[]>([])
const targetMac = ref('')
const bleFilter = ref<'all' | 'ibeacon' | 'named'>('all')
const connectingId = ref('')
const deviceServices = reactive<Record<string, CharInfo[]>>({})
let bleFoundHandler: ((res: any) => void) | null = null

// Step 1: 识别
const selectedDevice = ref<BleDevice | null>(null)
const manualUuid = ref('')

// Step 2: iBeacon
const activeBeaconUuid = ref('')
const beacons = ref<BeaconItem[]>([])

// ==================== 工具函数 ====================

function normalizeMac(mac: string): string {
  return mac.replace(/[:\-\s]/g, '').toUpperCase()
}

function ab2hex(buffer: ArrayBuffer): string {
  if (!buffer || buffer.byteLength === 0)
    return ''
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join(' ')
}

function parseIBeaconFromAdv(buffer: ArrayBuffer): IBeaconParsed | null {
  if (!buffer || buffer.byteLength < 25)
    return null
  const bytes = new Uint8Array(buffer)

  for (let i = 0; i < bytes.length - 24; i++) {
    if (bytes[i] === 0x4C && bytes[i + 1] === 0x00
      && bytes[i + 2] === 0x02 && bytes[i + 3] === 0x15) {
      const uuidBytes = Array.from(bytes.slice(i + 4, i + 20))
      const uuid = [
        uuidBytes.slice(0, 4),
        uuidBytes.slice(4, 6),
        uuidBytes.slice(6, 8),
        uuidBytes.slice(8, 10),
        uuidBytes.slice(10, 16),
      ].map(g => g.map(b => b.toString(16).padStart(2, '0')).join('')).join('-').toUpperCase()

      return {
        uuid,
        major: (bytes[i + 20] << 8) | bytes[i + 21],
        minor: (bytes[i + 22] << 8) | bytes[i + 23],
      }
    }
  }
  return null
}

function parseMacFromAdv(buffer: ArrayBuffer): string | null {
  if (!buffer || buffer.byteLength === 0)
    return null
  const bytes = new Uint8Array(buffer)
  let i = 0

  while (i < bytes.length) {
    const len = bytes[i]
    if (len === 0 || i + len >= bytes.length)
      break

    if (bytes[i + 1] === 0xFF && len >= 8) {
      const dataEnd = i + 1 + len
      const tail = Array.from(bytes.slice(dataEnd - 6, dataEnd))
      const forward = tail.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':')
      const reversed = [...tail].reverse().map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(':')
      return `${forward} / ${reversed}`
    }
    i += len + 1
  }
  return null
}

function isMatchTarget(device: BleDevice): boolean {
  const kw = normalizeMac(targetMac.value)
  if (!kw)
    return true

  // deviceId
  if (normalizeMac(device.deviceId).includes(kw))
    return true
  // MAC
  if (device.mac && normalizeMac(device.mac).includes(kw))
    return true
  // 名称
  if (device.name && device.name.toUpperCase().includes(kw))
    return true
  if (device.localName && device.localName.toUpperCase().includes(kw))
    return true
  // 广播数据
  if (device.advertisData) {
    const adv = device.advertisData.replace(/\s/g, '').toUpperCase()
    if (adv.includes(kw))
      return true
    const reversed = kw.match(/.{2}/g)?.reverse().join('') || ''
    if (reversed && adv.includes(reversed))
      return true
  }
  return false
}

function proximityText(p: number): string {
  if (p === 1)
    return '极近'
  if (p === 2)
    return '较近'
  if (p === 3)
    return '较远'
  return '未知'
}

function rssiPercent(rssi: number): number {
  // -100 → 0%, -30 → 100%
  return Math.max(0, Math.min(100, ((rssi + 100) / 70) * 100))
}

function addLog(msg: string, type: LogItem['type'] = 'info') {
  const now = new Date()
  const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
  logs.value.push({ time, msg, type })
  if (logs.value.length > 300)
    logs.value.splice(0, logs.value.length - 300)
}

function promisify(fn: any, opts: any = {}): Promise<any> {
  return new Promise((resolve, reject) => {
    fn({ ...opts, success: resolve, fail: reject })
  })
}

// ==================== 计算属性 ====================

const ibeaconDevices = computed(() => devices.value.filter(d => d.ibeacon))

const filteredBleDevices = computed(() => {
  let list = [...devices.value]
  if (bleFilter.value === 'ibeacon')
    list = list.filter(d => d.ibeacon)
  if (bleFilter.value === 'named')
    list = list.filter(d => !!(d.name || d.localName))
  list = list.filter(d => isMatchTarget(d))
  list.sort((a, b) => b.RSSI - a.RSSI)
  return list
})

const sortedBeacons = computed(() => [...beacons.value].sort((a, b) => b.rssi - a.rssi))

// ==================== 步骤导航 ====================

async function gotoStep(target: number) {
  if (scanning.value) {
    if (step.value === 0)
      await stopBleScan()
    if (step.value === 2)
      await stopBeaconScan()
  }
  errorMsg.value = ''
  step.value = target
}

// ==================== Step 0: BLE 扫描 ====================

async function startBleScan() {
  errorMsg.value = ''
  try {
    await promisify(uni.openBluetoothAdapter)
    addLog('蓝牙适配器已打开', 'success')
  } catch (err: any) {
    errorMsg.value = err?.errMsg || '蓝牙初始化失败'
    addLog(errorMsg.value, 'error')
    return
  }

  if (bleFoundHandler) {
    try {
      uni.offBluetoothDeviceFound?.(bleFoundHandler)
    } catch {}
  }

  bleFoundHandler = (res: any) => {
    const found = Array.isArray(res?.devices) ? res.devices : []
    for (const raw of found) {
      const item: BleDevice = {
        name: raw.name || raw.localName || '',
        localName: raw.localName || '',
        deviceId: raw.deviceId,
        mac: raw.advertisData ? parseMacFromAdv(raw.advertisData) || undefined : undefined,
        RSSI: raw.RSSI,
        advertisData: raw.advertisData ? ab2hex(raw.advertisData) || undefined : undefined,
        ibeacon: raw.advertisData ? parseIBeaconFromAdv(raw.advertisData) || undefined : undefined,
      }

      const idx = devices.value.findIndex(d => d.deviceId === item.deviceId)
      if (idx >= 0) {
        devices.value.splice(idx, 1, item)
      } else {
        devices.value.unshift(item)
        const tag = item.ibeacon ? ' [iBeacon]' : ''
        addLog(`发现: ${item.name || '未知'} RSSI=${item.RSSI}${tag}`, item.ibeacon ? 'success' : 'info')
      }
    }
  }

  uni.onBluetoothDeviceFound(bleFoundHandler)

  try {
    await promisify(uni.startBluetoothDevicesDiscovery, { allowDuplicatesKey: true })
    scanning.value = true
    addLog('BLE 扫描已启动', 'success')
  } catch (err: any) {
    errorMsg.value = err?.errMsg || '启动扫描失败'
    addLog(errorMsg.value, 'error')
  }
}

async function stopBleScan() {
  try {
    await promisify(uni.stopBluetoothDevicesDiscovery)
  } catch {}
  if (bleFoundHandler) {
    try {
      uni.offBluetoothDeviceFound?.(bleFoundHandler)
    } catch {}
    bleFoundHandler = null
  }
  scanning.value = false
  addLog('BLE 扫描已停止')
}

// ==================== 连接读取设备信息 ====================

/** Device Information Service 标准特征值 */
const DIS_CHARS: Record<string, string> = {
  '00002A23': 'System ID',
  '00002A24': 'Model Number',
  '00002A25': 'Serial Number',
  '00002A26': 'Firmware Rev',
  '00002A27': 'Hardware Rev',
  '00002A28': 'Software Rev',
  '00002A29': 'Manufacturer',
}

async function connectAndRead(device: BleDevice) {
  if (connectingId.value)
    return
  connectingId.value = device.deviceId
  addLog(`正在连接 ${device.name || device.deviceId}...`)

  try {
    // 先停止扫描（扫描和连接不能同时进行）
    const wasScan = scanning.value
    if (wasScan) {
      await promisify(uni.stopBluetoothDevicesDiscovery)
      scanning.value = false
    }

    // 连接
    await promisify(uni.createBLEConnection, { deviceId: device.deviceId })
    addLog('已连接', 'success')

    // 发现服务
    const svcRes = await promisify(uni.getBLEDeviceServices, { deviceId: device.deviceId })
    const services: any[] = svcRes.services || []
    addLog(`发现 ${services.length} 个服务`)

    const chars: CharInfo[] = []

    // 查找 Device Information Service (0x180A)
    const disService = services.find((s: any) =>
      s.uuid.toUpperCase().includes('180A'),
    )

    if (disService) {
      addLog(`找到设备信息服务: ${disService.uuid}`, 'success')
      const charRes = await promisify(uni.getBLEDeviceCharacteristics, {
        deviceId: device.deviceId,
        serviceId: disService.uuid,
      })

      for (const ch of (charRes.characteristics || [])) {
        if (!ch.properties?.read)
          continue
        try {
          const readRes = await promisify(uni.readBLECharacteristicValue, {
            deviceId: device.deviceId,
            serviceId: disService.uuid,
            characteristicId: ch.uuid,
          })

          // readBLECharacteristicValue 通过回调返回
          const value = await new Promise<string>((resolve) => {
            const handler = (result: any) => {
              if (result.characteristicId === ch.uuid) {
                ;(uni.offBLECharacteristicValueChange as any)?.(handler)
                const bytes = new Uint8Array(result.value)
                // 尝试解析为字符串
                const text = Array.from(bytes).map(b => String.fromCharCode(b)).join('')
                const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(':')
                resolve(isPrintable(text) ? text : hex)
              }
            }
            uni.onBLECharacteristicValueChange(handler as any)

            // 超时
            setTimeout(() => resolve('(read timeout)'), 3000)
          })

          const charKey = ch.uuid.slice(4, 8).toUpperCase()
          const name = DIS_CHARS[`0000${charKey}`] || ch.uuid
          chars.push({ name, value })
          addLog(`  ${name}: ${value}`)
        } catch {
          // 读取失败，跳过
        }
      }
    } else {
      addLog('该设备不支持 Device Information Service (0x180A)')

      // 列出所有服务 UUID 供参考
      for (const svc of services) {
        addLog(`  服务: ${svc.uuid}`)
      }
    }

    deviceServices[device.deviceId] = chars

    // 断开
    try {
      await promisify(uni.closeBLEConnection, { deviceId: device.deviceId })
    } catch {}
    addLog('已断开连接')

    // 恢复扫描
    if (wasScan) {
      await startBleScan()
    }
  } catch (err: any) {
    addLog(`连接失败: ${err?.errMsg || err?.message || '未知错误'}`, 'error')
    try {
      await promisify(uni.closeBLEConnection, { deviceId: device.deviceId })
    } catch {}
  } finally {
    connectingId.value = ''
  }
}

function isPrintable(str: string): boolean {
  return /^[\x20-\x7E]+$/.test(str) && str.length > 0
}

// ==================== Step 0 → Step 1/2 快捷操作 ====================

function handleDeviceTap(device: BleDevice) {
  // 展开/收起详情（当前靠模板展示，无需额外逻辑）
}

async function useAsBeacon(device: BleDevice) {
  if (!device.ibeacon)
    return
  if (scanning.value)
    await stopBleScan()
  selectedDevice.value = device
  activeBeaconUuid.value = device.ibeacon.uuid
  beacons.value = []
  step.value = 2
  addLog(`已选择信标 UUID: ${device.ibeacon.uuid}`, 'success')
}

// ==================== Step 1: 识别确认 ====================

async function confirmAndGo() {
  if (!selectedDevice.value?.ibeacon)
    return
  activeBeaconUuid.value = selectedDevice.value.ibeacon.uuid
  beacons.value = []
  step.value = 2
}

async function manualConfirmAndGo() {
  if (!manualUuid.value.trim())
    return
  activeBeaconUuid.value = manualUuid.value.trim()
  selectedDevice.value = null
  beacons.value = []
  step.value = 2
}

// ==================== Step 2: iBeacon 精确监听 ====================

function handleBeaconUpdate(res: { beacons: Array<{ uuid: string, major: number, minor: number, rssi: number, accuracy: number, proximity: number }> }) {
  for (const b of res.beacons) {
    const item: BeaconItem = {
      key: `${b.uuid}-${b.major}-${b.minor}`,
      uuid: b.uuid,
      major: b.major,
      minor: b.minor,
      rssi: b.rssi,
      accuracy: b.accuracy,
      proximity: b.proximity,
    }

    const idx = beacons.value.findIndex(x => x.key === item.key)
    if (idx >= 0) {
      beacons.value.splice(idx, 1, item)
    } else {
      beacons.value.unshift(item)
      addLog(`iBeacon: Major=${b.major} Minor=${b.minor} RSSI=${b.rssi} 距离=${b.accuracy.toFixed(1)}m`, 'success')
    }
  }
}

async function startBeaconScan() {
  errorMsg.value = ''
  if (!activeBeaconUuid.value) {
    errorMsg.value = '请先选择 iBeacon UUID'
    return
  }

  try {
    await promisify(uni.openBluetoothAdapter)
    addLog('蓝牙适配器已打开', 'success')
  } catch (err: any) {
    errorMsg.value = err?.errMsg || '蓝牙初始化失败'
    addLog(errorMsg.value, 'error')
    return
  }

  uni.onBeaconUpdate(handleBeaconUpdate as any)

  try {
    await new Promise<void>((resolve, reject) => {
      uni.startBeaconDiscovery({
        uuids: [activeBeaconUuid.value],
        success: () => resolve(),
        fail: (err: any) => reject(new Error(err?.errMsg || 'iBeacon 启动失败')),
      })
    })
    scanning.value = true
    addLog(`iBeacon 监听已启动: ${activeBeaconUuid.value}`, 'success')
  } catch (err: any) {
    errorMsg.value = err.message
    addLog(err.message, 'error')
  }
}

async function stopBeaconScan() {
  try {
    uni.stopBeaconDiscovery({})
  } catch {}
  try {
    (uni.offBeaconUpdate as any)?.(handleBeaconUpdate)
  } catch {}
  scanning.value = false
  addLog('iBeacon 监听已停止')
}

// ==================== 清理 ====================

onUnmounted(() => {
  if (step.value === 0 && scanning.value)
    stopBleScan()
  if (step.value === 2 && scanning.value)
    stopBeaconScan()
  try {
    uni.closeBluetoothAdapter({})
  } catch {}
})
</script>

<style lang="scss" scoped>
.page {
  min-height: 100vh;
  background: #f5f7fb;
  padding-bottom: 220rpx;
}

.section {
  padding: 16rpx 32rpx 0;
}

.list-section {
  padding: 16rpx 32rpx 0;
}

.card {
  border-radius: 24rpx;
  background: #fff;
  padding: 28rpx;
  box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.05);
}

.mt-12 {
  margin-top: 12rpx;
}
.mt-16 {
  margin-top: 16rpx;
}
.mt-24 {
  margin-top: 24rpx;
}
.text-xs {
  font-size: 20rpx;
}

// ===== 步骤条 =====
.steps {
  display: flex;
  align-items: center;
  gap: 4rpx;
  padding: 8rpx;
  border-radius: 24rpx;
  background: rgba(249, 115, 22, 0.06);
}

.step {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8rpx;
  padding: 18rpx 0;
  border-radius: 18rpx;
  transition: all 0.25s;
}

.step-num {
  width: 36rpx;
  height: 36rpx;
  border-radius: 50%;
  background: #e5e7eb;
  color: #9ca3af;
  font-size: 20rpx;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

.step-label {
  font-size: 22rpx;
  font-weight: 600;
  color: #9ca3af;
}

.step--active {
  background: #fff;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);

  .step-num {
    background: #f97316;
    color: #fff;
  }

  .step-label {
    color: #f97316;
  }
}

.step--done {
  .step-num {
    background: #10b981;
    color: #fff;
  }

  .step-label {
    color: #374151;
  }
}

// ===== 控制卡 =====
.control-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24rpx;
}

.control-main {
  flex: 1;
}

.title {
  font-size: 30rpx;
  font-weight: 700;
  color: #111827;
}

.desc {
  margin-top: 6rpx;
  font-size: 22rpx;
  color: #9ca3af;
}

.highlight-card {
  background: linear-gradient(135deg, #fef3c7, #fff7ed);
  border: 2rpx solid #f59e0b;
}

// ===== 输入 =====
.label {
  margin-bottom: 12rpx;
  font-size: 26rpx;
  font-weight: 600;
  color: #374151;
}

.input-wrap {
  display: flex;
  align-items: center;
  gap: 16rpx;
  border-radius: 16rpx;
  background: #f3f4f6;
  padding: 18rpx 20rpx;
}

.input {
  flex: 1;
  font-size: 26rpx;
  color: #111827;
}

.clear {
  font-size: 22rpx;
  color: #9ca3af;
}

.hint {
  margin-top: 12rpx;
  font-size: 22rpx;
  color: #9ca3af;
}

// ===== iOS 提示 =====
.ios-tip {
  border-radius: 20rpx;
  background: linear-gradient(135deg, #eff6ff, #f0f9ff);
  border: 2rpx solid #93c5fd;
  padding: 28rpx;
}

.ios-tip-title {
  font-size: 28rpx;
  font-weight: 700;
  color: #1d4ed8;
  margin-bottom: 12rpx;
}

.ios-tip-desc {
  font-size: 24rpx;
  line-height: 1.6;
  color: #3b82f6;
}

.error-box {
  border-radius: 16rpx;
  background: #fef2f2;
  padding: 20rpx 24rpx;
  font-size: 24rpx;
  color: #ef4444;
}

// ===== 筛选 =====
.tools {
  display: flex;
  gap: 12rpx;
  flex-wrap: wrap;
}

.chip {
  border-radius: 999rpx;
  background: #fff;
  padding: 10rpx 20rpx;
  font-size: 22rpx;
  font-weight: 600;
  color: #6b7280;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}

.chip--active {
  background: #f97316;
  color: #fff;
}

// ===== 空状态 =====
.empty {
  border-radius: 24rpx;
  background: #fff;
  padding: 60rpx 32rpx;
  text-align: center;
  box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.05);
}

.empty-title {
  font-size: 28rpx;
  font-weight: 600;
  color: #374151;
}

.empty-desc {
  margin-top: 12rpx;
  font-size: 22rpx;
  color: #9ca3af;
}

// ===== 设备卡 =====
.device-card {
  margin-bottom: 16rpx;
  border-radius: 24rpx;
  background: #fff;
  padding: 24rpx;
  box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.05);
  transition: all 0.2s;
}

.device-card--beacon {
  border-left: 6rpx solid #f97316;
}

.device-card--selectable {
  border: 2rpx solid transparent;

  &:active {
    background: #fefce8;
  }
}

.device-card--selected {
  border-color: #f97316;
  background: #fff7ed;
}

.device-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16rpx;
  margin-bottom: 12rpx;
}

.device-name {
  flex: 1;
  font-size: 28rpx;
  font-weight: 700;
  color: #111827;
  word-break: break-all;
}

.beacon-tag {
  display: inline-block;
  margin-left: 8rpx;
  padding: 2rpx 12rpx;
  border-radius: 8rpx;
  background: #fff7ed;
  font-size: 20rpx;
  font-weight: 600;
  color: #f97316;
  vertical-align: middle;
}

.proximity-tag {
  display: inline-block;
  margin-left: 8rpx;
  padding: 4rpx 16rpx;
  border-radius: 12rpx;
  font-size: 22rpx;
  font-weight: 700;
  vertical-align: middle;
}

.proximity-tag--near {
  background: #dcfce7;
  color: #16a34a;
}

.proximity-tag--mid {
  background: #fff7ed;
  color: #f97316;
}

.proximity-tag--far {
  background: #f3f4f6;
  color: #9ca3af;
}

.rssi {
  flex-shrink: 0;
  font-size: 24rpx;
  font-weight: 600;
  color: #f97316;
}

.info-row {
  display: flex;
  align-items: flex-start;
  gap: 12rpx;
  margin-top: 8rpx;
}

.info-label {
  width: 128rpx;
  flex-shrink: 0;
  font-size: 22rpx;
  font-weight: 600;
  color: #6b7280;
}

.info-value {
  flex: 1;
  font-size: 22rpx;
  color: #4b5563;
}

.mono {
  font-family: 'Courier New', Courier, monospace;
}

.break {
  word-break: break-all;
}

// ===== 操作按钮 =====
.device-actions {
  margin-top: 16rpx;
  padding-top: 16rpx;
  border-top: 2rpx solid #f3f4f6;
  display: flex;
  flex-wrap: wrap;
  gap: 12rpx;
}

.action-btn {
  padding: 12rpx 24rpx;
  border-radius: 16rpx;
  background: #f3f4f6;
  font-size: 22rpx;
  font-weight: 600;
  color: #374151;
  text-align: center;

  &:active {
    opacity: 0.7;
  }
}

.action-btn--primary {
  background: #f97316;
  color: #fff;
}

.action-btn--block {
  display: block;
  width: 100%;
}

// ===== 服务信息面板 =====
.services-panel {
  margin-top: 16rpx;
  padding: 16rpx;
  border-radius: 16rpx;
  background: #f0fdf4;
}

.services-title {
  margin-bottom: 8rpx;
  font-size: 22rpx;
  font-weight: 700;
  color: #16a34a;
}

// ===== RSSI 进度条 =====
.rssi-bar-wrap {
  flex: 1;
  height: 16rpx;
  border-radius: 8rpx;
  background: #f3f4f6;
  margin-top: 6rpx;
}

.rssi-bar {
  height: 100%;
  border-radius: 8rpx;
  background: linear-gradient(90deg, #f97316, #10b981);
  transition: width 0.3s;
}

// ===== 扫描动画 =====
.scanning-dot {
  display: inline-block;
  width: 12rpx;
  height: 12rpx;
  border-radius: 50%;
  background: #10b981;
  margin-left: 8rpx;
  vertical-align: middle;
  animation: pulse 1s infinite;
}

@keyframes pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.3;
  }
}

// ===== 日志 =====
.log-panel {
  margin-top: 32rpx;
  border-top: 2rpx solid #e5e7eb;
  background: #fafafa;
}

.log-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20rpx 24rpx;
}

.log-title {
  font-size: 24rpx;
  font-weight: 700;
  color: #6b7280;
}

.log-actions {
  display: flex;
  gap: 20rpx;
}

.log-action {
  font-size: 22rpx;
  color: #f97316;
}

.log-body {
  max-height: 400rpx;
  overflow: auto;
  padding: 0 24rpx 20rpx;
}

.log-item {
  padding-top: 8rpx;
  font-family: 'Courier New', Courier, monospace;
  font-size: 20rpx;
  color: #6b7280;
  word-break: break-all;
}

.log-item--error {
  color: #ef4444;
}
.log-item--success {
  color: #16a34a;
}
</style>
