'use strict';

const ROLE_COLORS = { leader: '#25d366', rear: '#42a5f5', medical: '#ef5350', stay: '#ffb74d', none: '#25d366' };
const ROLE_LABELS = { leader: '前導', rear: '殿後', medical: '醫療', stay: '留守', none: '' };
const QUICK_MSGS = ['到了', '等一下', '需要協助', '📍 我在這'];
const MORSE_UNIT = 120;

let e2eeCryptoKey = null;
let healthTimer = null;
let peerWatchTimer = null;
let batteryBound = false;
let sosHoldTimer = null;

function loadExtendedState() {
  state.offlineQueue = store.getJSON('p2p_offline_q', []);
  state.offlineQueueFull = false;
  state.e2eeEnabled = store.get('p2p_e2ee', '0') === '1';
  state.e2eeKey = store.get('p2p_e2ee_key', '');
  state.autoBatteryMode = store.get('p2p_auto_battery', '1') !== '0';
  state.quietHours = store.getJSON('p2p_quiet', { enabled: false, start: 22, end: 7 });
  state.largeUi = store.get('p2p_large_ui', '0') === '1';
  state.gloveMode = store.get('p2p_glove', '0') === '1';
  state.teamRole = store.get('p2p_team_role', 'none');
  state.roleDisplayName = store.get('p2p_role_name', '');
  state.onboardingDone = store.get('p2p_onboard', '1') === '1';
  state.loraForward = store.get('p2p_lora_forward', '0') === '1';
  if (store.get('opt_in_cleared_v113', '') !== '1') {
    store.set('p2p_lora_auto', '0');
    store.set('opt_in_cleared_v113', '1');
  }
  state.loraAutoFallback = store.get('p2p_lora_auto', '0') === '1';
  state.health = { relayMs: null, paths: [], mode: 'offline' };
  state.peerOfflineNotified = store.getJSON('p2p_peer_offline_n', {});
}

function getActivePaths() {
  const p = [];
  if (ws && ws.readyState === WebSocket.OPEN) p.push('relay');
  if (rtc.conns.some(c => c.dc && c.dc.readyState === 'open')) p.push('webrtc');
  if (state.loraAutoFallback && typeof window.meshBleLinked === 'function' && window.meshBleLinked()) p.push('lora');
  return p;
}

let _walkieStatusTimer = null;
let _walkiePairingStreak = 0;

function updateWalkieStatusBar() {
  const dot = $('walkieStatusDot');
  const text = $('walkieStatusText');
  const chip = $('walkieRelayChip');
  if (!text) return;
  const paths = getActivePaths();
  const p2p = paths.includes('webrtc');
  const relay = paths.includes('relay');
  let label = '近距未連線';
  let color = '#616161';
  if (p2p) {
    label = '近距已連線';
    color = '#2E7D32';
  } else if (relay) {
    label = '中繼已連線';
    color = '#00897B';
  } else if (ws && ws.readyState === WebSocket.CONNECTING) {
    label = '連線中…';
    color = '#EF6C00';
  } else if (state.loraAutoFallback && typeof window.meshBleLinked === 'function' && window.meshBleLinked()) {
    label = 'LoRa備援';
    color = '#6A1B9A';
  } else if (typeof needsOfflinePairing === 'function' && needsOfflinePairing()) {
    _walkiePairingStreak = Math.min(2, _walkiePairingStreak + 1);
    if (_walkiePairingStreak >= 2) {
      label = navigator.onLine !== false ? '區網·待配對' : '待配對';
      color = '#1565C0';
    }
  } else {
    _walkiePairingStreak = 0;
  }
  const urgent = p2p || relay || (ws && ws.readyState === WebSocket.CONNECTING);
  const apply = () => {
    text.textContent = `Pich2Pich · ${label}`;
    if (dot) dot.style.background = color;
    if (chip) chip.classList.toggle('hidden', !relay);
    const nameEl = $('walkieDeviceName');
    if (nameEl) {
      const n = (typeof state !== 'undefined' && state.name) ? state.name : '我的瀏覽器';
      nameEl.textContent = '本機：' + n;
    }
  };
  if (_walkieStatusTimer) clearTimeout(_walkieStatusTimer);
  if (urgent) apply();
  else _walkieStatusTimer = setTimeout(apply, 1500);
}

function updateHealthUI() {
  const el = $('healthBadge');
  const paths = getActivePaths();
  state.health.paths = paths;
  const ms = state.health.relayMs;
  const lat = ms != null ? `${ms}ms` : '—';
  const meshNote = (paths.includes('relay') && paths.includes('webrtc')) ? '+Mesh' : '';
  const tags = paths.length
    ? paths.map(p => ({ relay: '中繼', webrtc: 'P2P', lora: 'LoRa' }[p] || p)).join('+') + meshNote
    : (ws && ws.readyState === WebSocket.CONNECTING ? '連中繼…'
      : (typeof needsOfflinePairing === 'function' && needsOfflinePairing()
        ? (navigator.onLine !== false ? '區網·待配對' : '待配對')
        : (navigator.onLine !== false ? '待連線' : '離線')));
  state.health.mode = paths.includes('relay') ? 'relay' : (paths.includes('webrtc') ? 'webrtc' : (paths.includes('lora') ? 'lora' : 'offline'));
  if (el) {
    el.textContent = `${tags} · ${lat}`;
    el.className = 'health-badge ' + (paths.length ? 'ok' : 'warn');
  }
  updateWalkieStatusBar();
  updateDegradeBanner();
  if (typeof updateOfflineModeBanner === 'function') updateOfflineModeBanner();
  refreshConnectivityMatrix();
}

function setMatrixCell(id, text, ok) {
  const el = $(id);
  if (!el) return;
  el.textContent = text;
  el.className = ok ? 'ok' : (ok === false ? 'warn' : '');
}

function guessLanGatewayFromHost() {
  const h = location.hostname || '';
  if (/^192\.168\.\d+\.\d+$/.test(h) || /^10\.\d+\.\d+\.\d+$/.test(h)) {
    const parts = h.split('.');
    parts[3] = '1';
    return parts.join('.');
  }
  return store.get('p2p_venue_gw', '') || '';
}

async function probeTcpHint(host, port, timeoutMs) {
  // Browsers cannot raw-TCP; use fetch/WebSocket race as soft probe.
  return new Promise((resolve) => {
    let done = false;
    const finish = (ok) => { if (!done) { done = true; resolve(ok); } };
    const t = setTimeout(() => finish(false), timeoutMs);
    try {
      const proto = location.protocol === 'https:' ? 'wss' : 'ws';
      if (port === 8000) {
        const wsTry = new WebSocket(`${proto}://${host}:8000/ws`);
        wsTry.onopen = () => { clearTimeout(t); try { wsTry.close(); } catch {} finish(true); };
        wsTry.onerror = () => { clearTimeout(t); finish(false); };
        return;
      }
      fetch(`http://${host}:${port}/`, { mode: 'no-cors', cache: 'no-store' })
        .then(() => { clearTimeout(t); finish(true); })
        .catch(() => { clearTimeout(t); finish(false); });
    } catch {
      clearTimeout(t);
      finish(false);
    }
  });
}

async function probeVenueLan() {
  const status = $('venueLanStatus');
  const gwInp = $('venueLanGateway');
  const relayInp = $('venueLanRelayUrl');
  let gw = (gwInp && gwInp.value.trim()) || guessLanGatewayFromHost();
  if (gwInp && gw && !gwInp.value) gwInp.value = gw;
  if (!gw) {
    if (status) status.textContent = '閘道：FAIL（請手動填閘道 IP）· 裝置互通：待 QR／中繼';
    return;
  }
  store.set('p2p_venue_gw', gw);
  if (status) status.textContent = '探測中…';
  const [p80, p443, p8000] = await Promise.all([
    probeTcpHint(gw, 80, 1500),
    probeTcpHint(gw, 443, 1500),
    probeTcpHint(gw, 8000, 2000),
  ]);
  const gwOk = p80 || p443 || p8000;
  const sug = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${gw}:8000/ws`;
  if (relayInp && !relayInp.value) relayInp.value = sug;
  store.set('p2p_venue_relay', (relayInp && relayInp.value) || sug);
  const peerOk = typeof rtc !== 'undefined' && rtc.conns.some(c => c.dc && c.dc.readyState === 'open');
  if (status) {
    status.textContent =
      `閘道：${gwOk ? 'OK' : 'FAIL'}（:80 ${p80 ? '✓' : '×'} :443 ${p443 ? '✓' : '×'} :8000 ${p8000 ? '✓' : '×'}）· ` +
      `裝置互通：${peerOk ? 'OK（P2P）' : '待 QR／區網配對'} · ${gwOk ? '建議區網模式' : '可能隔離／訪客網 → 改 Direct'}`;
  }
  refreshConnectivityMatrix();
}

function bindVenueLanUi() {
  const probe = $('venueLanProbeBtn');
  if (probe) probe.onclick = () => probeVenueLan();
  const relayBtn = $('venueLanRelayBtn');
  if (relayBtn) {
    relayBtn.onclick = () => {
      const url = ($('venueLanRelayUrl') && $('venueLanRelayUrl').value.trim()) ||
        store.get('p2p_venue_relay', '');
      if (!url) { alert('請先探測或填入 LAN 中繼 URL'); return; }
      store.set('p2p_venue_relay', url);
      if (typeof state !== 'undefined') {
        state.relayUrl = url;
        store.set('p2p_relay_url', url);
        const inp = $('relayUrl');
        if (inp) inp.value = url;
      }
      if (typeof connectRelay === 'function') connectRelay(true);
      else alert('已填入中繼 URL：' + url + '，請到設定連線');
    };
  }
  const qr = $('venueLanQrBtn');
  if (qr) {
    qr.onclick = () => {
      const btn = $('hotspotQrBtn') || $('openHotspotPairBtn') || document.querySelector('[data-conn-mode="hotspot"]');
      if (btn) btn.click();
      else if (typeof setConnMode === 'function') setConnMode('hotspot');
    };
  }
  const gw = $('venueLanGateway');
  if (gw && !gw.value) gw.value = guessLanGatewayFromHost();
  const ri = $('venueLanRelayUrl');
  if (ri && !ri.value) ri.value = store.get('p2p_venue_relay', '');
}

function refreshConnectivityMatrix() {
  const relay = ws && ws.readyState === WebSocket.OPEN;
  const p2p = typeof rtc !== 'undefined' && rtc.conns.some(c => c.dc && c.dc.readyState === 'open');
  const ble = typeof window.meshBleLinked === 'function' && window.meshBleLinked();
  const lora = !!(state.loraAutoFallback && ble);
  const meshOn = relay && (p2p || lora);
  setMatrixCell('mxRelayChrome', relay ? '✅ 已連中繼' : '待連', relay);
  setMatrixCell('mxQrChrome', p2p ? '✅ P2P 已配對' : '待 QR 配對', p2p);
  setMatrixCell('mxRouterChrome', p2p ? '✅ 區網 P2P' : '連路由器後 QR', p2p);
  setMatrixCell('mxMeshChrome', meshOn ? '✅ 多跳可用' : (relay ? '⚠️ 待 P2P/LoRa' : '需中繼+P2P'), meshOn);
  setMatrixCell('mxLoraChrome', lora ? '✅ LoRa備援' : (ble ? '⚠️ BLE 已連・備援關' : (relay ? '⚠️ MQTT 橋接' : '待連')), lora || ble || relay);
}

function updateDegradeBanner() {
  const el = $('degradeBanner');
  if (!el) return;
  if (typeof needsOfflinePairing === 'function' && needsOfflinePairing()) {
    const p2p = rtc.conns.some(c => c.dc && c.dc.readyState === 'open');
    if (p2p) { el.classList.add('hidden'); return; }
    el.innerHTML = '📡 <b>無外網模式（正常）</b>：接熱點方請按「📋 貼上配對碼」→ 複製回應碼給開熱點方 → 對方再貼一次';
    el.classList.remove('hidden');
    return;
  }
  const relay = ws && ws.readyState === WebSocket.OPEN;
  const relayBusy = ws && ws.readyState === WebSocket.CONNECTING;
  const p2p = rtc.conns.some(c => c.dc && c.dc.readyState === 'open');
  if (relay) { el.classList.add('hidden'); return; }
  if (relayBusy) {
    el.innerHTML = '⏳ 正在連中繼…（有網時可用 6 碼配對）';
    el.classList.remove('hidden');
    return;
  }
  if (p2p) {
    el.innerHTML = '⚡ 中繼斷線，已自動改用 <b>WebRTC P2P</b>';
    el.classList.remove('hidden');
    return;
  }
  el.innerHTML = '⚠️ 未連線 · <button type="button" class="link-btn" id="degradeQrBtn">掃 QR 配對</button> 或輸入6碼';
  el.classList.remove('hidden');
  const b = $('degradeQrBtn');
  if (b) b.onclick = () => { if (window.openScannerAuto) openScannerAuto(); else showPage('walkie'); };
}

function pingRelayHealth() {
  if (!ws || ws.readyState !== WebSocket.OPEN) { state.health.relayMs = null; updateHealthUI(); return; }
  const t = Date.now();
  ws.send(JSON.stringify({ kind: 'PING', ts: t, from: myCode() }));
}

function onPong(ts) {
  state.health.relayMs = Math.max(0, Date.now() - (ts || Date.now()));
  updateHealthUI();
}

function startHealthMonitor() {
  if (healthTimer) clearInterval(healthTimer);
  healthTimer = setInterval(() => { pingRelayHealth(); updateHealthUI(); }, 5000);
  updateHealthUI();
}

async function initE2eeKey() {
  e2eeCryptoKey = null;
  if (!state.e2eeEnabled || !state.e2eeKey) return;
  try {
    const enc = new TextEncoder();
    const base = await crypto.subtle.importKey('raw', enc.encode(state.e2eeKey.padEnd(16, '0').slice(0, 32)), 'PBKDF2', false, ['deriveKey']);
    e2eeCryptoKey = await crypto.subtle.deriveKey(
      { name: 'PBKDF2', salt: enc.encode('pich2pich-e2ee'), iterations: 80000, hash: 'SHA-256' },
      base, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']
    );
  } catch (e) { console.warn('E2EE key init failed', e); }
}

async function encryptPayload(text) {
  if (!e2eeCryptoKey || !text) return null;
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, e2eeCryptoKey, new TextEncoder().encode(text));
  const buf = new Uint8Array(iv.length + ct.byteLength);
  buf.set(iv); buf.set(new Uint8Array(ct), iv.length);
  return btoa(String.fromCharCode(...buf));
}

async function decryptPayload(b64) {
  if (!e2eeCryptoKey || !b64) return null;
  const bin = atob(b64);
  const buf = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  const iv = buf.slice(0, 12);
  const ct = buf.slice(12);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, e2eeCryptoKey, ct);
  return new TextDecoder().decode(pt);
}

async function encryptPayloadBytes(b64OrBytes) {
  if (!e2eeCryptoKey) return null;
  const bytes = typeof b64OrBytes === 'string'
    ? Uint8Array.from(atob(b64OrBytes), c => c.charCodeAt(0))
    : b64OrBytes;
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, e2eeCryptoKey, bytes);
  const buf = new Uint8Array(iv.length + ct.byteLength);
  buf.set(iv); buf.set(new Uint8Array(ct), iv.length);
  return btoa(String.fromCharCode(...buf));
}

async function decryptPayloadBytes(b64) {
  if (!e2eeCryptoKey || !b64) return null;
  const bin = atob(b64);
  const buf = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
  const iv = buf.slice(0, 12);
  const ct = buf.slice(12);
  const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, e2eeCryptoKey, ct);
  return new Uint8Array(pt);
}

function e2eeQrPayload() {
  return 'p2p-e2ee:' + (state.relayRoom || 'team1') + ':' + (state.e2eeKey || '');
}

function parseE2eeQrPayload(raw) {
  const t = (raw || '').trim();
  if (!t.startsWith('p2p-e2ee:')) return null;
  const body = t.slice('p2p-e2ee:'.length);
  const i = body.indexOf(':');
  if (i <= 0) return null;
  return { room: body.slice(0, i), key: body.slice(i + 1) };
}

async function prepareOutboundMsg(msg) {
  const out = { ...msg };
  const near = (ws && ws.readyState === WebSocket.OPEN) ||
    (typeof rtc !== 'undefined' && rtc.conns.some(c => c.dc && c.dc.readyState === 'open'));
  if ((state.loraForward || (state.loraAutoFallback && !near)) &&
      (out.type === 'TEXT' || out.type === 'TELEMETRY') && !out.to) {
    out.via = 'lora';
  }
  if (state.e2eeEnabled && e2eeCryptoKey) {
    if (out.type === 'TEXT' && out.text) {
      const enc = await encryptPayload(out.text);
      if (enc) { out.enc = true; out.text = enc; out.plain = undefined; }
    } else if ((out.type === 'IMAGE' || out.type === 'VOICE_CLIP') && out.blob) {
      const enc = await encryptPayloadBytes(out.blob);
      if (enc) { out.enc = true; out.blob = enc; out.mime = 'application/octet-stream'; }
    }
  }
  return out;
}

async function decodeInboundMsg(msg) {
  if (msg.enc && e2eeCryptoKey) {
    if (msg.text) {
      try {
        const plain = await decryptPayload(msg.text);
        if (plain) return { ...msg, text: '🔒 ' + plain, enc: false };
      } catch { return { ...msg, text: '🔒 [無法解密]' }; }
    }
    if (msg.blob) {
      try {
        const bytes = await decryptPayloadBytes(msg.blob);
        if (bytes) {
          let mime = msg.mime || 'application/octet-stream';
          if (msg.type === 'IMAGE') mime = 'image/jpeg';
          if (msg.type === 'VOICE_CLIP') mime = 'audio/webm';
          const b64 = btoa(String.fromCharCode(...bytes));
          return { ...msg, blob: b64, mime, enc: false };
        }
      } catch { return { ...msg, text: '🔒 [無法解密媒體]' }; }
    }
  }
  return msg;
}

const OFFLINE_Q_MAX = 100;

function enqueueOffline(item) {
  state.offlineQueue.push(item);
  let dropped = 0;
  if (state.offlineQueue.length > OFFLINE_Q_MAX) {
    dropped = state.offlineQueue.length - OFFLINE_Q_MAX;
    state.offlineQueue = state.offlineQueue.slice(-OFFLINE_Q_MAX);
    state.offlineQueueFull = true;
  }
  store.setJSON('p2p_offline_q', state.offlineQueue);
  if (dropped > 0) updateOfflineQueueFullBanner();
}

function updateOfflineQueueFullBanner() {
  const el = $('offlineQueueFullBanner');
  if (!el) return;
  if (state.offlineQueueFull) {
    el.classList.remove('hidden');
    el.onclick = () => {
      state.offlineQueueFull = false;
      el.classList.add('hidden');
    };
  } else {
    el.classList.add('hidden');
  }
}

async function flushOfflineQueue() {
  if (!hasMeshLink() || !state.offlineQueue.length) {
    if (window.flushSosQueue) flushSosQueue();
    return;
  }
  const q = state.offlineQueue.splice(0);
  store.setJSON('p2p_offline_q', state.offlineQueue);
  const near = typeof hasNearLink === 'function' ? hasNearLink() : true;
  const loraOnly = !near && state.loraAutoFallback && typeof hasLoraLink === 'function' && hasLoraLink();
  const failed = [];
  for (const m of q) {
    try {
      if (loraOnly && typeof isLoraWireType === 'function' && !isLoraWireType(m.type || 'TEXT')) {
        failed.push(m);
        continue;
      }
      const msg = await prepareOutboundMsg(m);
      if (loraOnly) {
        msg.id = msg.id || uuid();
        msg.ttl = msg.ttl ?? state.meshTtl;
        msg.sid = msg.sid || state.id;
        msg.name = msg.name || state.name;
        msg.ts = msg.ts || Date.now();
        state.seen.add(msg.id);
        const ok = window.meshtasticSendWire ? await meshtasticSendWire(msg) : false;
        if (!ok) { failed.push(m); continue; }
      } else {
        const out = originate(msg);
        if (!out || out._sent === false) { failed.push(m); continue; }
      }
      markMessageStatus(m.clientMsgId, 'sent');
    } catch {
      failed.push(m);
    }
  }
  if (failed.length) {
    state.offlineQueue.push(...failed);
    if (state.offlineQueue.length > OFFLINE_Q_MAX) {
      state.offlineQueueFull = true;
      state.offlineQueue = state.offlineQueue.slice(-OFFLINE_Q_MAX);
      updateOfflineQueueFullBanner();
    }
    store.setJSON('p2p_offline_q', state.offlineQueue);
  }
  if (window.flushSosQueue) flushSosQueue();
}

async function meshSend(partial, clientMsgId) {
  const base = { ...partial };
  if (clientMsgId) base.clientMsgId = clientMsgId;
  const near = typeof hasNearLink === 'function' ? hasNearLink() : hasMeshLink();
  const loraOnly = !near && state.loraAutoFallback && typeof hasLoraLink === 'function' && hasLoraLink();
  if (!hasMeshLink() || (loraOnly && typeof isLoraWireType === 'function' && !isLoraWireType(base.type || 'TEXT'))) {
    enqueueOffline({ ...base, qid: uuid(), ts: Date.now() });
    if (clientMsgId) markMessageStatus(clientMsgId, 'queued');
    updateDegradeBanner();
    return null;
  }
  const msg = await prepareOutboundMsg(base);
  if (loraOnly) {
    msg.id = msg.id || uuid();
    msg.ttl = msg.ttl ?? state.meshTtl;
    msg.sid = state.id;
    msg.name = state.name;
    msg.ts = msg.ts || Date.now();
    state.seen.add(msg.id);
    const ok = window.meshtasticSendWire ? await meshtasticSendWire(msg) : false;
    if (!ok) {
      enqueueOffline({ ...base, qid: uuid(), ts: Date.now() });
      if (clientMsgId) markMessageStatus(clientMsgId, 'queued');
      updateDegradeBanner();
      return null;
    }
    if (clientMsgId) {
      state.pendingAcks[msg.id] = clientMsgId;
      markMessageStatus(clientMsgId, 'sent');
    }
    return msg;
  }
  const sent = originate(msg);
  if (!sent || sent._sent === false) {
    enqueueOffline({ ...base, qid: uuid(), ts: Date.now() });
    if (clientMsgId) markMessageStatus(clientMsgId, 'queued');
    updateDegradeBanner();
    return null;
  }
  if (clientMsgId) {
    state.pendingAcks[sent.id] = clientMsgId;
    markMessageStatus(clientMsgId, 'sent');
  }
  return sent;
}

function markMessageStatus(clientMsgId, status) {
  if (!clientMsgId) return;
  let changed = false;
  Object.keys(state.convs).forEach(k => {
    state.convs[k].forEach(e => {
      if (e.msgId === clientMsgId) { e.status = status; changed = true; }
    });
  });
  if (changed) { persistConvs(); if (state.chatInRoom) renderChat(); }
}

function markDelivered(refId) {
  let changed = false;
  Object.keys(state.convs).forEach(k => {
    state.convs[k].forEach(e => {
      if (e.serverMsgId === refId || e.msgId === state.pendingAcks[refId]) {
        e.status = 'delivered'; changed = true;
      }
    });
  });
  delete state.pendingAcks[refId];
  if (changed) { persistConvs(); if (state.chatInRoom) renderChat(); }
}

function sendAckFor(msg) {
  if (!msg.id || !msg.sid) return;
  originate({ type: 'ACK', refId: msg.id, to: msg.sid, ttl: 2 });
}

function isQuietHours() {
  if (!state.quietHours.enabled) return false;
  const h = new Date().getHours();
  const s = state.quietHours.start;
  const e = state.quietHours.end;
  if (s <= e) return h >= s && h < e;
  return h >= s || h < e;
}

function playVoiceNotifyExtended() {
  if (isQuietHours()) {
    if (navigator.vibrate) navigator.vibrate(40);
    return;
  }
  playVoiceNotify();
}

function applyUiModes() {
  document.body.classList.toggle('large-ui', state.largeUi);
  document.body.classList.toggle('glove-mode', state.gloveMode);
}

function peerMarkerColor(p) {
  if (p && p.role && ROLE_COLORS[p.role]) return ROLE_COLORS[p.role];
  return '#ef6c00';
}

function telemetryExtras() {
  const o = {
    role: state.teamRole || 'none',
    roleName: state.roleDisplayName || '',
  };
  if (state.pressureHpa != null) o.pressure = +state.pressureHpa.toFixed(2);
  if (state.estimatedFloor != null) o.estFloor = state.estimatedFloor;
  return o;
}

let barometerSensor = null;
let baroCal = store.getJSON('p2p_baro_cal', null);

function updateBaroUI() {
  const el = $('baroStatus');
  if (!el) return;
  if (state.pressureHpa == null) {
    el.textContent = typeof Barometer === 'undefined'
      ? '此裝置不支援氣壓計，請手動輸入樓層'
      : '啟用氣壓計中…';
    return;
  }
  const est = state.estimatedFloor != null ? ` · 估算 ${state.estimatedFloor}F` : '';
  el.textContent = `氣壓 ${state.pressureHpa.toFixed(1)} hPa${est}`;
}

function initBarometer() {
  if (typeof Barometer === 'undefined') {
    updateBaroUI();
    return;
  }
  try {
    barometerSensor = new Barometer({ frequency: 0.25 });
    barometerSensor.addEventListener('reading', () => {
      state.pressureHpa = barometerSensor.pressure;
      if (!baroCal && state.manualFloor != null) {
        baroCal = { floor: state.manualFloor, hpa: state.pressureHpa };
        store.setJSON('p2p_baro_cal', baroCal);
      }
      if (baroCal) {
        state.estimatedFloor = baroCal.floor + Math.round((baroCal.hpa - state.pressureHpa) / 0.38);
        const sf = $('selfFloor');
        if (sf) sf.textContent = state.estimatedFloor + 'F';
      }
      updateBaroUI();
    });
    barometerSensor.start();
  } catch {
    updateBaroUI();
  }
}

function calibrateBarometerFloor() {
  if (state.pressureHpa == null) return alert('尚無氣壓讀數，請稍候');
  baroCal = { floor: state.manualFloor, hpa: state.pressureHpa };
  store.setJSON('p2p_baro_cal', baroCal);
  state.estimatedFloor = state.manualFloor;
  updateBaroUI();
  alert(`已校正：${state.manualFloor}F = ${state.pressureHpa.toFixed(1)} hPa`);
}

function imHere() {
  if (state.self) {
    originate({ type: 'TELEMETRY', lat: state.self.lat, lon: state.self.lon, floor: state.manualFloor, ...telemetryExtras() });
  }
  const loc = state.self ? `${state.self.lat.toFixed(5)}, ${state.self.lon.toFixed(5)}` : '定位中';
  const label = ROLE_LABELS[state.teamRole] || '';
  const name = state.roleDisplayName || state.name;
  meshSend({ type: 'TEXT', to: null, text: `📍 ${name}${label ? '(' + label + ')' : ''} 我在這 · ${loc}` });
  rescueMorseSos(false);
  showMapHint('已送出位置與「我在這」');
}

function rescueMorseSos(fullSos) {
  const pattern = fullSos
    ? [3, 1, 3, 1, 3, 3, 3, 1, 3, 1, 3, 3, 3, 1, 3, 1, 3]
    : [2, 1, 2, 1, 2];
  let i = 0;
  const ov = $('sosFlashOverlay');
  const step = () => {
    if (i >= pattern.length) {
      if (ov) { ov.classList.add('hidden'); ov.classList.remove('on'); }
      return;
    }
    const on = i % 2 === 0;
    if (ov) { ov.classList.remove('hidden'); ov.classList.toggle('on', on); }
    if (on && navigator.vibrate) navigator.vibrate(MORSE_UNIT * pattern[i]);
    i++;
    setTimeout(step, MORSE_UNIT * pattern[i - 1]);
  };
  step();
}

function bindSosLongPress(btn) {
  if (!btn) return;
  btn.textContent = '廣播 SOS';
  let holdFired = false;
  let activePointer = null;
  const fire = () => {
    broadcastSos(false);
    if (state.e2eeEnabled) rescueMorseSos(true);
    else if (typeof toggleSosFlash === 'function') toggleSosFlash();
  };
  // 主路徑：一鍵點擊；長按 3 秒為可選確認（同一動作，避免雙發）
  btn.addEventListener('click', () => {
    if (holdFired) {
      holdFired = false;
      return;
    }
    fire();
  });
  btn.addEventListener('pointerdown', (e) => {
    if (activePointer != null) return;
    activePointer = e.pointerId;
    btn.classList.add('holding');
    btn.textContent = '長按確認中…';
    sosHoldTimer = setTimeout(() => {
      holdFired = true;
      activePointer = null;
      btn.classList.remove('holding');
      btn.textContent = '廣播 SOS';
      fire();
    }, 3000);
  });
  const cancel = (e) => {
    if (activePointer != null && e.pointerId !== activePointer) return;
    activePointer = null;
    if (sosHoldTimer) clearTimeout(sosHoldTimer);
    sosHoldTimer = null;
    btn.classList.remove('holding');
    btn.textContent = '廣播 SOS';
  };
  btn.addEventListener('pointerup', cancel);
  btn.addEventListener('pointercancel', cancel);
  btn.addEventListener('pointerleave', cancel);
}

function bindSos(btn) {
  bindSosLongPress(btn);
}

function startBatteryMonitor() {
  if (batteryBound || !navigator.getBattery) return;
  batteryBound = true;
  navigator.getBattery().then(b => {
    const check = () => {
      if (!state.autoBatteryMode) return;
      if (b.level <= 0.2 && !b.charging) {
        state.geoMode = 'eco';
        store.set('p2p_geo_mode', 'eco');
        state.keepScreenOn = false;
        store.set('p2p_keep_screen', '0');
        const ks = $('keepScreenSwitch');
        if (ks) ks.checked = false;
        document.querySelectorAll('input[name=geoMode]').forEach(r => { r.checked = r.value === 'eco'; });
        restartTelemetry();
        startGeolocation();
        updateWakeLock();
        showMapHint('低電量：已切換省電模式');
      }
    };
    b.addEventListener('levelchange', check);
    b.addEventListener('chargingchange', check);
    check();
  }).catch(() => {});
}

function startPeerOfflineWatch() {
  if (peerWatchTimer) clearInterval(peerWatchTimer);
  peerWatchTimer = setInterval(() => {
    const now = Date.now();
    Object.keys(state.peers).forEach(sid => {
      const p = state.peers[sid];
      const age = now - (p.ts || 0);
      if (age < 5 * 60000) return;
      if (state.peerOfflineNotified[sid] && now - state.peerOfflineNotified[sid] < 30 * 60000) return;
      state.peerOfflineNotified[sid] = now;
      store.setJSON('p2p_peer_offline_n', state.peerOfflineNotified);
      notify('👤 隊友離線', `${p.name} 已 ${Math.round(age / 60000)} 分鐘未更新位置`);
    });
  }, 60000);
}

function renderQuickMsgs() {
  const box = $('quickMsgs');
  if (!box) return;
  box.innerHTML = '';
  QUICK_MSGS.forEach(t => {
    const b = document.createElement('button');
    b.type = 'button';
    b.className = 'quick-msg-btn';
    b.textContent = t;
    b.onclick = async () => {
      if (t.includes('我在這')) { imHere(); return; }
      if (typeof sendChat === 'function') await sendChat({ type: 'TEXT', text: t });
      else await meshSend({ type: 'TEXT', text: t, to: null });
    };
    box.appendChild(b);
  });
}

function maybeShowOnboarding() {
  /* 已關閉開頁三步介紹 */
}

function isLoraMsg(msg) {
  return msg.via === 'lora' || msg.sid === 'meshtastic-node';
}

function loraBadgeHtml() {
  return '<span class="lora-tag">經 LoRa 送出</span>';
}

function outboundViaLora() {
  if (state.loraForward) return true;
  if (!state.loraAutoFallback) return false;
  const near = (ws && ws.readyState === WebSocket.OPEN) ||
    (typeof rtc !== 'undefined' && rtc.conns.some(c => c.dc && c.dc.readyState === 'open'));
  return !near && typeof window.meshBleLinked === 'function' && window.meshBleLinked();
}

function generateTeamE2eeKey() {
  const bytes = new Uint8Array(12);
  crypto.getRandomValues(bytes);
  const key = btoa(String.fromCharCode(...bytes)).replace(/[+/=]/g, '').slice(0, 16);
  state.e2eeKey = key;
  state.e2eeEnabled = true;
  store.set('p2p_e2ee_key', key);
  store.set('p2p_e2ee', '1');
  const inp = $('e2eeKey');
  if (inp) inp.value = key;
  const sw = $('e2eeSwitch');
  if (sw) sw.checked = true;
  initE2eeKey();
  alert('已產生隊伍密鑰，請按「複製給隊友」分享');
}

async function copyTeamE2eeKey() {
  if (!state.e2eeKey) generateTeamE2eeKey();
  const text = e2eeQrPayload();
  try {
    await navigator.clipboard.writeText(text);
    alert('已複製 QR 內容（p2p-e2ee:…）');
  } catch {
    prompt('複製給隊友：', text);
  }
}

function showE2eeQrModal() {
  if (!state.e2eeKey) generateTeamE2eeKey();
  const el = $('e2eeQrModal');
  const holder = $('e2eeQrHolder');
  if (!el || !holder) return;
  const lib = typeof qrLib === 'function' ? qrLib() : (window.qrcode || null);
  if (!lib || typeof renderQrCanvas !== 'function') { alert('QR 程式庫未載入'); return; }
  const qr = lib(0, 'M');
  qr.addData(e2eeQrPayload());
  qr.make();
  holder.innerHTML = '';
  holder.appendChild(renderQrCanvas(qr));
  el.classList.remove('hidden');
}

function importE2eeFromQrText(raw) {
  const p = parseE2eeQrPayload(raw);
  if (!p || !p.key) { alert('格式錯誤'); return false; }
  state.e2eeKey = p.key;
  state.e2eeEnabled = true;
  state.relayRoom = p.room || state.relayRoom;
  store.set('p2p_e2ee_key', p.key);
  store.set('p2p_e2ee', '1');
  store.set('p2p_relay_room', state.relayRoom);
  const inp = $('e2eeKey');
  if (inp) inp.value = p.key;
  const sw = $('e2eeSwitch');
  if (sw) sw.checked = true;
  const roomInp = $('relayRoom');
  if (roomInp) roomInp.value = state.relayRoom;
  initE2eeKey();
  return true;
}

function bindExtendedUI() {
  applyUiModes();
  renderQuickMsgs();

  const e2eeSw = $('e2eeSwitch');
  const e2eeKey = $('e2eeKey');
  if (e2eeSw) {
    e2eeSw.checked = state.e2eeEnabled;
    e2eeSw.onchange = async (e) => {
      state.e2eeEnabled = e.target.checked;
      store.set('p2p_e2ee', state.e2eeEnabled ? '1' : '0');
      await initE2eeKey();
    };
  }
  if (e2eeKey) {
    e2eeKey.value = state.e2eeKey;
    e2eeKey.oninput = async (e) => {
      state.e2eeKey = e.target.value;
      store.set('p2p_e2ee_key', state.e2eeKey);
      await initE2eeKey();
    };
  }
  const e2eeGen = $('e2eeGenKeyBtn');
  if (e2eeGen) e2eeGen.onclick = () => generateTeamE2eeKey();
  const e2eeCopy = $('e2eeCopyKeyBtn');
  if (e2eeCopy) e2eeCopy.onclick = () => copyTeamE2eeKey();
  const e2eeQr = $('e2eeShowQrBtn');
  if (e2eeQr) e2eeQr.onclick = () => showE2eeQrModal();
  const e2eeImport = $('e2eeImportBtn');
  if (e2eeImport) e2eeImport.onclick = () => {
    const raw = prompt('貼上掃描結果（p2p-e2ee:房間:密鑰）');
    if (raw && importE2eeFromQrText(raw)) alert('已匯入 E2EE 密鑰');
  };

  const qh = $('quietHoursSwitch');
  const qhs = $('quietStart');
  const qhe = $('quietEnd');
  if (qh) {
    qh.checked = state.quietHours.enabled;
    qh.onchange = e => { state.quietHours.enabled = e.target.checked; store.setJSON('p2p_quiet', state.quietHours); };
  }
  if (qhs) { qhs.value = state.quietHours.start; qhs.oninput = e => { state.quietHours.start = +e.target.value; store.setJSON('p2p_quiet', state.quietHours); }; }
  if (qhe) { qhe.value = state.quietHours.end; qhe.oninput = e => { state.quietHours.end = +e.target.value; store.setJSON('p2p_quiet', state.quietHours); }; }

  const ab = $('autoBatterySwitch');
  if (ab) {
    ab.checked = state.autoBatteryMode;
    ab.onchange = e => { state.autoBatteryMode = e.target.checked; store.set('p2p_auto_battery', state.autoBatteryMode ? '1' : '0'); };
  }

  const lu = $('largeUiSwitch');
  const gm = $('gloveModeSwitch');
  if (lu) { lu.checked = state.largeUi; lu.onchange = e => { state.largeUi = e.target.checked; store.set('p2p_large_ui', state.largeUi ? '1' : '0'); applyUiModes(); }; }
  if (gm) { gm.checked = state.gloveMode; gm.onchange = e => { state.gloveMode = e.target.checked; store.set('p2p_glove', state.gloveMode ? '1' : '0'); applyUiModes(); }; }

  const tr = $('teamRole');
  const rn = $('roleDisplayName');
  if (tr) {
    tr.value = state.teamRole;
    tr.onchange = e => {
      state.teamRole = e.target.value;
      store.set('p2p_team_role', state.teamRole);
      if (state.self) setSelfOnMap(state.self.lat, state.self.lon);
    };
  }
  if (rn) {
    rn.value = state.roleDisplayName;
    rn.oninput = e => { state.roleDisplayName = e.target.value; store.set('p2p_role_name', state.roleDisplayName); };
  }

  const im = $('imHereBtn');
  if (im) im.onclick = imHere;

  const lf = $('loraForwardSwitch');
  if (lf) {
    lf.checked = state.loraForward;
    lf.onchange = e => {
      state.loraForward = e.target.checked;
      store.set('p2p_lora_forward', state.loraForward ? '1' : '0');
    };
  }
  const laf = $('loraAutoFallbackSwitch');
  if (laf) {
    laf.checked = !!state.loraAutoFallback;
    laf.onchange = e => {
      state.loraAutoFallback = e.target.checked;
      store.set('p2p_lora_auto', state.loraAutoFallback ? '1' : '0');
    };
  }
  bindVenueLanUi();

  bindSos($('sosBroadcastBtn'));

  const obDone = $('onboardDoneBtn');
  if (obDone) obDone.onclick = () => {
    state.onboardingDone = true;
    store.set('p2p_onboard', '1');
    $('onboardModal').classList.add('hidden');
  };
  document.querySelectorAll('[data-onboard-next]').forEach((b, i, arr) => {
    b.onclick = () => {
      const step = +b.dataset.onboardNext;
      document.querySelectorAll('.onboard-step').forEach(s => s.classList.add('hidden'));
      const next = $('onboardStep' + step);
      if (next) next.classList.remove('hidden');
      if (step > arr.length) { obDone.click(); }
    };
  });
}

async function initExtendedFeatures() {
  loadExtendedState();
  await initE2eeKey();
  bindExtendedUI();
  startHealthMonitor();
  startBatteryMonitor();
  startPeerOfflineWatch();
  initBarometer();
}

window.initExtendedFeatures = initExtendedFeatures;
window.updateHealthUI = updateHealthUI;
window.refreshConnectivityMatrix = refreshConnectivityMatrix;
window.onPong = onPong;
window.flushOfflineQueue = flushOfflineQueue;
window.meshSend = meshSend;
window.markDelivered = markDelivered;
window.sendAckFor = sendAckFor;
window.decodeInboundMsg = decodeInboundMsg;
window.playVoiceNotifyExtended = playVoiceNotifyExtended;
window.peerMarkerColor = peerMarkerColor;
window.telemetryExtras = telemetryExtras;
window.ROLE_COLORS = ROLE_COLORS;
window.ROLE_LABELS = ROLE_LABELS;
window.isLoraMsg = isLoraMsg;
window.loraBadgeHtml = loraBadgeHtml;
window.bindSos = bindSos;
window.updateOfflineQueueFullBanner = updateOfflineQueueFullBanner;
window.calibrateBarometerFloor = calibrateBarometerFloor;
window.initBarometer = initBarometer;

const DIST_MAX_ACC_M = 50;
const DIST_FUZZY_ACC_M = 25;
const DIST_PEER_STALE_MS = 15000;

function rssiProximityHint(rssi, nearWifi) {
  if (!nearWifi || rssi == null || rssi >= 0) return null;
  if (rssi >= -55) return '很近（Wi‑Fi 訊號強）';
  if (rssi >= -68) return '較近（Wi‑Fi）';
  if (rssi >= -78) return '中等距離（Wi‑Fi）';
  return '較遠（Wi‑Fi 訊號弱）';
}

/** 與 APK DistanceEstimate 對齊的距離顯示 */
function formatPeerDistance(self, peer, opts) {
  opts = opts || {};
  const nearWifi = !!opts.nearWifi;
  const wifiRssi = opts.wifiRssi;
  const rssiHint = rssiProximityHint(wifiRssi, nearWifi);

  if (opts.manualOverride && opts.manualDistanceM != null) {
    return {
      line: `${opts.manualDistanceM} 公尺（手動）`,
      hint: rssiHint || '手動指定距離；警報仍依 GPS 計算'
    };
  }

  if (!self || peer.lat == null || peer.lon == null) {
    return { line: '—', hint: rssiHint || '等待定位…' };
  }
  const age = Date.now() - (peer.ts || 0);
  if (age > DIST_PEER_STALE_MS) {
    return { line: '—', hint: rssiHint || '對方定位逾時' };
  }
  const meters = distanceM(self.lat, self.lon, peer.lat, peer.lon) + (opts.offsetM || 0);
  const metersClamped = Math.max(0, meters);
  const sAcc = self.accuracy != null ? self.accuracy : 20;
  const pAcc = peer.acc != null ? peer.acc : 20;

  if (sAcc > DIST_MAX_ACC_M || pAcc > DIST_MAX_ACC_M) {
    const parts = [];
    if (sAcc > DIST_MAX_ACC_M) parts.push(`GPS 精度不足（±${Math.round(sAcc)}m）`);
    if (pAcc > DIST_MAX_ACC_M) parts.push('對方 GPS 精度不足');
    if (rssiHint) parts.push(rssiHint);
    return { line: '—', hint: parts.join(' · ') };
  }

  const margin = Math.round(sAcc + pAcc);
  const fuzzy = sAcc > DIST_FUZZY_ACC_M || pAcc > DIST_FUZZY_ACC_M || margin > DIST_FUZZY_ACC_M;
  let line;
  if (fuzzy) {
    if (metersClamped < 15) line = '約 15 公尺內';
    else if (metersClamped < 30) line = '約 30 公尺內';
    else if (metersClamped < 50) line = '約 50 公尺內';
    else if (metersClamped < 100) line = '約 100 公尺內';
    else if (metersClamped < 200) line = '約 200 公尺內';
    else line = `約 ${Math.ceil(metersClamped / 100) * 100} 公尺內`;
  } else {
    const off = opts.offsetM ? `，校正${opts.offsetM > 0 ? '+' : ''}${opts.offsetM}` : '';
    line = `${Math.round(metersClamped)} 公尺（±${margin}${off}）`;
  }

  const hintParts = [];
  if (fuzzy) {
    hintParts.push(`GPS 誤差約 ±${margin} m，僅供參考`);
    if (metersClamped < 30) hintParts.push('近距離請參考 Wi‑Fi 訊號');
    if (opts.offsetM) hintParts.push(`已套用偏移 ${opts.offsetM > 0 ? '+' : ''}${opts.offsetM} m`);
  }
  if (rssiHint) hintParts.push(rssiHint);
  return { line, hint: hintParts.length ? hintParts.join(' · ') : null };
}

window.formatPeerDistance = formatPeerDistance;
