'use strict';

/* ============================================================
 * MyClaw Mobile client
 * Connects to the SAME /ws protocol as the desktop web UI.
 * - Sends commands (chat, set_autonomy, set_provider, set_model, ...)
 * - Renders the LIVE event stream from the shared EventBus
 *   (this is the "takeover": mobile sees exactly what the agent
 *    emits, and can drive the agent's settings in real time)
 * ============================================================ */

const $ = (id) => document.getElementById(id);

let ws = null;
let reconnectTimer = null;
let currentAssistant = null;   // streaming assistant bubble
let currentReasoning = null;   // reasoning sub-block
let currentToolCard = null;    // tool card (collapsed once its result arrives)
let finalReplyText = '';       // last final text from the agent (may be empty)
let finalException = null;     // closing error / interrupt reason
let mFiles = [];               // generated files accumulated during a run
let pendingApprovalId = null;
let connected = false;
// Task management: each task keeps its own conversation context.
let tasks = [];
let activeTaskId = null;
// Task id whose conversation the events pane currently shows.
let paneTaskId = null;
// Session token (minted by /api/login). Stored in localStorage so the
// login survives app restarts (no need to retype the long password every
// time); sessionStorage is kept as a fallback for older pages.
let sessionToken = localStorage.getItem('mclaw_token') || sessionStorage.getItem('mclaw_session');
let authRequired = false;

/* ---------- WebSocket connection ---------- */

function wsUrl() {
  const override = (localStorage.getItem('myclaw_server_url') || '').trim();
  let url;
  if (override) {
    let base = override.replace(/^wss?:\/\//, '').replace(/\/.*$/, '');
    if (base) {
      const proto = (override.startsWith('wss') || location.protocol === 'https:') ? 'wss' : 'ws';
      url = proto + '://' + base + '/ws';
    } else {
      const proto = location.protocol === 'https:' ? 'wss' : 'ws';
      url = proto + '://' + location.host + '/ws';
    }
  } else {
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    url = proto + '://' + location.host + '/ws';
  }
  if (sessionToken) {
    url += '?token=' + encodeURIComponent(sessionToken);
  }
  return url;
}

function setConnected(on) {
  connected = on;
  const dot = $('conn-dot');
  dot.className = 'dot ' + (on ? 'online' : 'offline');
  $('conn-info').textContent = on ? ('已连接 ' + wsUrl()) : '未连接';
}

function connect() {
  const url = wsUrl();
  try {
    ws = new WebSocket(url);
  } catch (e) {
    scheduleReconnect();
    return;
  }

  ws.onopen = () => {
    setConnected(true);
    toast('已连接.');
    send({ type: 'get_status' });
    send({ type: 'task_list' });
    fetchSkills();
  };

  ws.onmessage = (ev) => {
    let msg;
    try { msg = JSON.parse(ev.data); } catch { return; }
    handleEvent(msg);
  };

  ws.onclose = () => {
    setConnected(false);
    // If auth is required and we had a (now invalid) token, surface the login
    // screen instead of endlessly reconnecting.
    if (authRequired && sessionToken) {
      sessionToken = null;
      localStorage.removeItem('mclaw_token');
      sessionStorage.removeItem('mclaw_session');
      showLogin();
      return;
    }
    scheduleReconnect();
  };

  ws.onerror = () => {
    setConnected(false);
  };
}

function scheduleReconnect() {
  if (reconnectTimer) return;
  reconnectTimer = setTimeout(() => {
    reconnectTimer = null;
    connect();
  }, 2000);
}

function send(obj) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(obj));
    return true;
  }
  toast('未连接，无法发送');
  return false;
}

/* ---------- Event rendering ---------- */

function scrollDown() {
  const el = $('events');
  el.scrollTop = el.scrollHeight;
}

// "HH:MM:SS" wall-clock tag for every non-user block, so the timeline can be
// re-read in strict order. Returns a DOM span (text content, never HTML).
function makeTimeTag() {
  const d = new Date();
  const p = (n) => (n < 10 ? '0' : '') + n;
  const s = document.createElement('span');
  s.className = 'hist-time-m';
  s.textContent = p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
  return s;
}

// Prepend a time tag to a container (bubble/card/reasoning) at the very top.
function stampTime(el) {
  el.insertBefore(makeTimeTag(), el.firstChild);
  return el;
}

function addBubble(role, text, withTime) {
  const wrap = document.createElement('div');
  wrap.className = 'bubble ' + role;
  if (withTime) stampTime(wrap); // non-user block → wall-clock tag
  const body = document.createElement('div');
  body.className = 'bubble-body';
  body.textContent = text || '';
  wrap.appendChild(body);
  $('events').appendChild(wrap);
  scrollDown();
  return body;
}

function ensureAssistant() {
  if (currentAssistant) return currentAssistant;
  const wrap = document.createElement('div');
  wrap.className = 'bubble assistant';
  stampTime(wrap); // non-user block → wall-clock tag
  const body = document.createElement('div');
  body.className = 'bubble-body';
  wrap.appendChild(body);
  $('events').appendChild(wrap);
  currentAssistant = body;
  scrollDown();
  return body;
}

function ensureReasoning() {
  if (currentReasoning) return currentReasoning;
  const wrap = document.createElement('div');
  wrap.className = 'reasoning'; // expanded (visible) while thinking
  stampTime(wrap); // non-user block → wall-clock tag
  const head = document.createElement('div');
  head.className = 'reasoning-head';
  head.textContent = '推理';
  head.onclick = () => wrap.classList.toggle('collapsed'); // manual toggle
  const body = document.createElement('div');
  body.className = 'reasoning-body';
  wrap.appendChild(head);
  wrap.appendChild(body);
  $('events').appendChild(wrap);
  currentReasoning = body;
  scrollDown();
  return body;
}

function addCard(cls, title, content, ok) {
  const card = document.createElement('div');
  card.className = 'card ' + cls + (ok === false ? ' fail' : '');
  if (title) {
    const t = document.createElement('div');
    t.className = 'card-title';
    t.textContent = title;
    t.onclick = () => card.classList.toggle('collapsed'); // manual toggle
    card.appendChild(t);
  }
  const c = document.createElement('div');
  c.className = 'card-body';
  c.textContent = content || '';
  card.appendChild(c);
  stampTime(card); // non-user block → wall-clock tag (sits above the title)
  $('events').appendChild(card);
  scrollDown();
  return card;
}

// Closing summary bubble with a compressed body: smaller/dimmer text after
// the "已结束，" prefix, full text on tap (title).
function addClosingSummaryM(text) {
  const wrap = document.createElement('div');
  wrap.className = 'bubble assistant closing-summary';
  stampTime(wrap);
  const body = document.createElement('div');
  body.className = 'bubble-body';
  const pre = document.createElement('span');
  pre.className = 'closing-prefix';
  pre.textContent = '已结束，';
  const cont = document.createElement('span');
  cont.className = 'closing-body';
  cont.textContent = text || '';
  cont.title = text || '';
  body.appendChild(pre);
  body.appendChild(cont);
  wrap.appendChild(body);
  $('events').appendChild(wrap);
  scrollDown();
}

// Paperclip icon appended after the closing summary: hidden file list,
// click opens a popup with the generated files (download links).
function addFilesIconM(paths) {
  const wrap = document.createElement('div');
  wrap.className = 'bubble task-started files-icon-m';
  stampTime(wrap);
  const btn = document.createElement('button');
  btn.type = 'button';
  btn.className = 'files-icon-btn';
  btn.textContent = '📎 ' + paths.length;
  btn.onclick = () => openFilesPopupM(paths);
  wrap.appendChild(btn);
  $('events').appendChild(wrap);
  scrollDown();
}

// Small popup (modal) listing the generated files with download links.
function openFilesPopupM(paths) {
  const overlay = document.createElement('div');
  overlay.className = 'files-popup-overlay';
  const box = document.createElement('div');
  box.className = 'files-popup';
  const head = document.createElement('div');
  head.className = 'files-popup-head';
  head.innerHTML = '<span>📎 生成的文件（' + paths.length + '）</span>' +
    '<button type="button" class="files-popup-close" aria-label="关闭">✕</button>';
  const list = document.createElement('div');
  list.className = 'files-popup-list';
  paths.forEach((p) => {
    const name = String(p).split(/[\\/]/).pop() || p;
    const tokenQ = '&token=' + encodeURIComponent(sessionToken || '');
    const row = document.createElement('div');
    row.className = 'files-popup-row';
    if (/\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(p)) {
      const img = document.createElement('img');
      img.className = 'file-img-m';
      img.src = '/api/files?path=' + encodeURIComponent(p) + '&inline=1' + tokenQ;
      img.alt = name;
      img.loading = 'lazy';
      row.appendChild(img);
    }
    const a = document.createElement('a');
    a.className = 'file-dl';
    a.href = '/api/files?path=' + encodeURIComponent(p) + tokenQ;
    a.download = name;
    a.textContent = '⬇ ' + name;
    a.title = p;
    row.appendChild(a);
    list.appendChild(row);
  });
  box.appendChild(head);
  box.appendChild(list);
  overlay.appendChild(box);
  document.body.appendChild(overlay);
  function close() {
    if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
    document.removeEventListener('keydown', esc);
  }
  function esc(e) { if (e.key === 'Escape') close(); }
  overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
  box.querySelector('.files-popup-close').addEventListener('click', close);
  document.addEventListener('keydown', esc);
}

function handleEvent(ev) {
  switch (ev.type) {
    case 'thinking':
      // New task iteration → start a fresh assistant bubble for this round,
      // so each round's reasoning + text carries its own time point (the
      // timeline must be strictly ordered, not accumulated into one block).
      currentAssistant = null;
      currentReasoning = null;
      if (ev.reasoning) {
        const r = ensureReasoning();
        r.textContent += ev.reasoning;
        scrollDown();
      }
      break;

    case 'reasoning_delta':
      ensureReasoning().textContent += ev.delta;
      if (currentReasoning) currentReasoning.parentElement.classList.remove('collapsed');
      scrollDown();
      break;

    case 'text_delta':
      // content starts → thinking done, keep the reasoning panel expanded
      // so the thought content stays visible (tap to collapse manually)
      if (currentReasoning) {
        currentReasoning = null;
      }
      ensureAssistant().textContent += ev.delta;
      scrollDown();
      break;

    case 'content':
      // final complete answer (may be empty when the model finished purely
      // via tool calls). Store it; the closing summary is rendered on `done`.
      currentAssistant = null;
      currentReasoning = null;
      finalReplyText = (ev.text || '').trim();
      break;

    case 'tool_call':
      currentAssistant = null;
      currentToolCard = addCard('tool', '🔧 ' + ev.name, ev.args, true); // visible while running
      break;

    case 'tool_result':
      // execution finished → collapse the args card, show the result card
      if (currentToolCard) { currentToolCard.classList.add('collapsed'); currentToolCard = null; }
      addCard('result', (ev.success ? '✅ ' : '❌ ') + ev.name, ev.result, ev.success);
      break;

    case 'step_status':
      // Live task step status (P1-5): show only transitions to running/error.
      if (ev.status === 'running') {
        addCard('plan', '🔄 ' + ev.label, '执行中…', true);
      } else if (ev.status === 'error') {
        addCard('result', '❌ ' + ev.label, ev.detail || '失败', false);
      }
      break;

    case 'plan':
      addCard('plan', '📋 计划', ev.steps.map((s, i) => (i + 1) + '. ' + s).join('\n'), true);
      break;

    case 'rag_context':
      if (ev.sources && ev.sources.length) {
        addCard('rag', '📚 引用 ' + ev.sources.length + ' 个知识源',
          ev.sources.map((s) => '• ' + (s.doc_title || s.doc_path)).join('\n'), true);
      }
      break;

    case 'usage':
      $('sb-provider').textContent = 'tokens: ' + (ev.total_tokens || 0);
      break;

    case 'approval_request':
      showApproval(ev);
      break;

    case 'loop_warning':
      toast('⚠️ ' + ev.message, 4000);
      break;

    case 'retry':
      addCard('plan', '↻ ' + (ev.stage === 'llm' ? 'LLM' : '知识库') + ' 调用失败，重试中（' + (ev.attempt || 1) + '/' + (ev.max || 1) + '）', ev.reason || '', true);
      break;

    case 'error':
      currentAssistant = null;
      finalException = ev.message;
      break;

    case 'task_aborted':
      currentAssistant = null;
      currentReasoning = null;
      finalException = ev.message;
      break;

    case 'done':
      currentAssistant = null;
      currentReasoning = null;
      // Unified closing summary:
      //  异常 → 出现异常，{原因} / 正常+内容 → 已结束，{内容} / 无内容 → 已结束
      if (finalException) {
        addBubble('assistant', '出现异常，' + finalException, true);
      } else if (finalReplyText) {
        addClosingSummaryM(finalReplyText);
      } else {
        addBubble('assistant', '已结束', true);
      }
      // Generated files: hidden behind a compact paperclip icon (opens popup).
      if (mFiles && mFiles.length > 0) addFilesIconM(mFiles);
      mFiles = [];
      break;

    case 'files':
      // Files generated by write_file during this run — accumulate silently;
      // the paperclip icon on `done` opens the list.
      if (ev.paths && ev.paths.length > 0) {
        mFiles = mFiles.concat(ev.paths);
      }
      break;

    case 'status':
      applyStatus(ev);
      break;

    // --- Task management ---
    case 'task_list':
      renderTaskList(ev.tasks || []);
      break;
    case 'task_created':
    case 'task_switched':
    case 'task_deleted':
      send({ type: 'task_list' });
      break;

    default:
      // documents / stats / config — desktop-oriented, ignore on mobile
      break;
  }
}

/* ---------- Approval (takeover control) ---------- */

function showApproval(ev) {
  pendingApprovalId = ev.id;
  $('approval-body').textContent = (ev.tool_name || '工具') + '\n' + (ev.args || '') + '\n' + (ev.reason || '');
  $('approval').classList.remove('hidden');
  if (typeof fitComposer === 'function') fitComposer();
}

function hideApproval() { $('approval').classList.add('hidden'); if (typeof fitComposer === 'function') fitComposer(); }

$('approve-yes').onclick = () => {
  if (pendingApprovalId) send({ type: 'approve', id: pendingApprovalId, approved: true });
  hideApproval();
};
$('approve-no').onclick = () => {
  if (pendingApprovalId) send({ type: 'approve', id: pendingApprovalId, approved: false });
  hideApproval();
};

/* ---------- Settings drawer ---------- */

function openDrawer() { $('drawer').classList.add('open'); $('drawer-mask').classList.remove('hidden'); }
function closeDrawer() { $('drawer').classList.remove('open'); $('drawer-mask').classList.add('hidden'); }

$('gear-btn').onclick = openDrawer;
$('drawer-close').onclick = closeDrawer;
$('drawer-mask').onclick = closeDrawer;

let statusCache = { autonomy: 'supervised', providers: [], model: '' };

function applyStatus(s) {
  // The server broadcasts two different shapes as type 'status':
  // runtime status (autonomy/model/providers) and progress notices
  // (message only). Only the runtime shape carries providers — ignore
  // progress notices here, otherwise the dropdown gets wiped to empty.
  if (!s || !Array.isArray(s.providers)) return;
  // Providers arrive as [{id, name}] — `name` is the config label shown in
  // the UI, `id` is the routing value.
  const activeId = s.active_provider || null;
  const active = (s.providers || []).find((p) => p.id === activeId);
  statusCache = {
    autonomy: s.autonomy || 'supervised',
    providers: s.providers || [],
    model: s.model || '',
    activeProvider: activeId,
  };
  $('sb-model').textContent = 'model: ' + (s.model || '—');
  // Show the provider's display name only (no "provider:" prefix).
  $('sb-provider').textContent = active ? active.name : (s.model || '—');
  $('sb-autonomy').textContent = 'autonomy: ' + (s.autonomy || '—');

  // provider options — value = id, text = display name.
  const sel = $('provider-sel');
  sel.innerHTML = '';
  (s.providers || []).forEach((p) => {
    const o = document.createElement('option');
    o.value = p.id; o.textContent = p.name || p.id;
    sel.appendChild(o);
  });
  // Select the active (last-used) provider, else the first one.
  if (activeId && (s.providers || []).some((p) => p.id === activeId)) {
    sel.value = activeId;
  } else if (s.providers && s.providers.length > 0) {
    sel.value = s.providers[0].id;
  }

  if (s.model) $('model-input').value = s.model;

  // autonomy segmented highlight
  document.querySelectorAll('#autonomy-seg .seg').forEach((b) => {
    b.classList.toggle('active', b.dataset.v === s.autonomy);
  });
}

document.querySelectorAll('#autonomy-seg .seg').forEach((b) => {
  b.onclick = () => {
    document.querySelectorAll('#autonomy-seg .seg').forEach((x) => x.classList.remove('active'));
    b.classList.add('active');
  };
});

$('apply-btn').onclick = () => {
  const autonomy = document.querySelector('#autonomy-seg .seg.active');
  if (autonomy) send({ type: 'set_autonomy', level: autonomy.dataset.v });
  const provider = $('provider-sel').value;
  if (provider) send({ type: 'set_provider', name: provider });
  const model = $('model-input').value.trim();
  if (model) send({ type: 'set_model', model });
  const m = $('drawer-msg');
  m.textContent = '已发送设置';
  setTimeout(() => { m.textContent = ''; }, 1500);
};

$('status-refresh').onclick = () => send({ type: 'get_status' });
$('status-btn').onclick = () => { send({ type: 'get_status' }); openDrawer(); };
$('clear-btn').onclick = () => { send({ type: 'clear_context', task_id: activeTaskId || undefined }); toast('已清空当前任务上下文'); };

/* ---------- Task management (each task = own context) ---------- */

function renderTaskList(list) {
  tasks = list || [];
  const sel = $('task-sel');
  const prev = sel.value;
  sel.innerHTML = '';
  // No tasks yet -> hide the whole task bar (first chat creates the first
  // task; until then there is nothing to switch between).
  const bar = $('task-bar');
  if (bar) bar.style.display = (tasks.length === 0) ? 'none' : '';
  let hasActive = false;
  tasks.forEach((t) => {
    const opt = document.createElement('option');
    opt.value = t.id;
    opt.textContent = (t.active ? '● ' : '') + t.name + '（' + t.messages + ' 条）';
    sel.appendChild(opt);
    if (t.active) { activeTaskId = t.id; hasActive = true; }
  });
  if (hasActive) sel.value = activeTaskId;
  else if (prev && tasks.some((t) => t.id === prev)) sel.value = prev;
  else if (tasks.length) sel.value = tasks[0].id;

  // Replay the active task's conversation when the pane shows a different
  // task (initial connect, task switch from the select or another client).
  const active = tasks.find((x) => x.active);
  if (active) {
    if (active.id !== paneTaskId) {
      renderTaskHistory(active);
    }
  } else {
    paneTaskId = null;
  }
}

// Replay a task's stored conversation (user text + final assistant answers).
function renderTaskHistory(task) {
  clearEventsPane();
  paneTaskId = task ? task.id : null;
  if (task && task.history && task.history.length > 0) {
    task.history.forEach((m) => {
      addBubble(m.role === 'user' ? 'user' : 'assistant', m.content || '', m.role !== 'user');
    });
  }
}

function clearEventsPane() {
  $('events').innerHTML = '';
  currentAssistant = null;
  currentReasoning = null;
}

$('task-sel').addEventListener('change', () => {
  const id = $('task-sel').value;
  if (!id || id === activeTaskId) return;
  send({ type: 'task_switch', id });
  clearEventsPane();
  const t = tasks.find((x) => x.id === id);
  addBubble('assistant', '已切换到任务：' + (t ? t.name : id) + '（独立上下文）', true);
});

$('task-new').onclick = () => {
  const name = prompt('新任务名称（用于区分上下文）：', '');
  if (name === null) return;
  send({ type: 'task_create', name: name || undefined });
  clearEventsPane();
  addBubble('assistant', '已创建新任务并切换到它', true);
};

/* ---------- Composer ---------- */

function doSend() {
  const el = $('input');
  const text = el.value.trim();
  if (!text) return;
  addBubble('user', text);
  addTaskStarted();
  currentAssistant = null;
  currentReasoning = null;
  finalReplyText = '';
  finalException = null;
  mFiles = [];
  // The pane now shows this task's live conversation — don't replay history
  // over it on the next task_list.
  if (activeTaskId) paneTaskId = activeTaskId;
  // "全列表块显示用例" — render every block locally (no WS, no real run).
  if (/全列表块显示用例/.test(text)) {
    runAllBlocksDemoM();
    el.value = '';
    el.style.height = 'auto';
    return;
  }
  send({ type: 'chat', message: text, task_id: activeTaskId || undefined });
  el.value = '';
  el.style.height = 'auto';
}

// Demo: replay every renderable block in the mobile events pane with
// realistic content — no task is actually run, no tools are executed.
function runAllBlocksDemoM() {
  const queue = [
    { t: 'thinking', d: { iteration: 1 } },
    { t: 'reasoning_delta', d: { delta: '分析任务：\n1. 检查目录\n2. 生成入口页\n3. 生成两个游戏\n4. 验证\n开始执行。' } },
    { t: 'text_delta', d: { delta: '先查看目录，确认环境。' } },
    { t: 'tool_call', d: { name: 'list_directory', args: '{"path":"/Users/admin/cube"}' } },
    { t: 'tool_result', d: { name: 'list_directory', result: '[DIR]  games\n[FILE]  README.md', success: true } },
    { t: 'retry', d: { stage: 'kshare', attempt: 1, max: 2, reason: 'connection reset' } },
    { t: 'retry', d: { stage: 'llm', attempt: 2, max: 3, reason: 'timeout' } },
    { t: 'tool_call', d: { name: 'read_file', args: '{"path":"/Users/admin/cube/missing.txt"}' } },
    { t: 'tool_result', d: { name: 'read_file', result: 'No such file or directory', success: false } },
    { t: 'thinking', d: { iteration: 2 } },
    { t: 'reasoning_delta', d: { delta: '目标不存在，改写入新文件：入口页 + 俄罗斯方块 + 贪吃蛇。' } },
    { t: 'text_delta', d: { delta: '开始写入三个页面文件。' } },
    { t: 'tool_call', d: { name: 'write_file', args: '{"path":"/Users/admin/cube/games/index.html","content":"..."}' } },
    { t: 'tool_result', d: { name: 'write_file', result: 'index.html written: 2646 bytes', success: true } },
    { t: 'files', d: { paths: ['/Users/admin/cube/games/index.html', '/Users/admin/cube/games/tetris.html'] } },
    { t: 'plan', d: { steps: ['查看目录', '创建入口页', '创建俄罗斯方块', '创建贪吃蛇', '验证'] } },
    { t: 'loop_warning', d: { message: '模型可能陷入重复循环：同一工具调用已重复 3 次', repeated_count: 3 } },
    { t: 'approval_request', d: { id: 'demo-approval-1', tool_name: 'run_command', args: '{"command":"rm -rf /tmp/demo"}', reason: 'This tool can modify your system. Approve to proceed.' } },
    { t: 'content', d: { text: '已完成三个页面文件：\n\nindex.html · 入口页\ntetris.html · 俄罗斯方块\nsnake.html · 贪吃蛇\n\n代码已全部写入磁盘。' } },
    { t: 'done', d: {} }
  ];
  let i = 0;
  (function step() {
    if (i >= queue.length) return;
    const item = queue[i++];
    handleEvent({ type: item.t, ...item.d });
    setTimeout(step, 350);
  })();
}

// "任务已启动" pill right after the user's message — the timeline opens with
// an explicit start before the first round of 思考→决策→执行.
function addTaskStarted() {
  const wrap = document.createElement('div');
  wrap.className = 'bubble task-started';
  stampTime(wrap); // non-user block → wall-clock tag
  const body = document.createElement('div');
  body.className = 'bubble-body';
  body.textContent = '🚀 任务已启动';
  wrap.appendChild(body);
  $('events').appendChild(wrap);
  scrollDown();
}

$('send-btn').onclick = doSend;
$('input').addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    doSend();
  }
});
$('input').addEventListener('input', (e) => {
  e.target.style.height = 'auto';
  e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
  handleSkillInput();
});

/* ---------- Skill palette (mobile, P1-3) ---------- */

let skills = [];
function fetchSkills() {
  fetch('/api/skills')
    .then((r) => r.json())
    .then((d) => { skills = (d && d.skills) || []; })
    .catch(() => { skills = []; });
}

function handleSkillInput() {
  const pal = $('skill-palette-m');
  const v = $('input').value.trimStart();
  if (v === '/') {
    if (!skills.length) fetchSkills();
    if (skills.length) {
      pal.innerHTML = skills.map((s, i) =>
        '<div class="skill-item-m" data-index="' + i + '">' +
          '<div class="skill-name-m">/' + s.name + '</div>' +
          '<div class="skill-desc-m">' + (s.description || '') + '</div>' +
        '</div>'
      ).join('');
      pal.classList.remove('hidden');
      pal.querySelectorAll('.skill-item-m').forEach((it) => {
        it.onclick = () => insertSkill(skills[parseInt(it.dataset.index, 10)]);
      });
    }
  } else if (!v.startsWith('/')) {
    pal.classList.add('hidden');
  }
}

function insertSkill(skill) {
  const el = $('input');
  const trimmed = el.value.trimStart();
  const sp = trimmed.indexOf(' ');
  const args = sp >= 0 ? trimmed.slice(sp).trim() : '';
  let prompt = skill.prompt || '';
  if (args && prompt.indexOf('{input}') !== -1) prompt = prompt.replace('{input}', args);
  else if (args) prompt = prompt + '\n' + args;
  el.value = prompt;
  $('skill-palette-m').classList.add('hidden');
  el.focus();
  el.style.height = 'auto';
  el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}

$('input').addEventListener('keydown', (e) => { if (e.key === 'Escape') $('skill-palette-m').classList.add('hidden'); });

/* ---------- Voice input (mobile, P1-4) ---------- */

let recognition = null;
let recording = false;

function initSpeech() {
  // Browser path: Web Speech API (SpeechRecognition).
  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (SR) {
    recognition = new SR();
    recognition.lang = 'zh-CN';
    recognition.interimResults = false;
    recognition.maxAlternatives = 1;
    recognition.onresult = (ev) => {
      const text = ev.results[0][0].transcript || '';
      if (text) {
        const el = $('input');
        el.value = el.value.trimEnd() + (el.value ? ' ' : '') + text;
        el.style.height = 'auto';
        el.style.height = Math.min(el.scrollHeight, 120) + 'px';
        toast('🎤 ' + text);
      }
      stopRecording();
    };
    recognition.onerror = () => stopRecording();
    recognition.onend = () => stopRecording();
  }
  // Android WebView path: Kotlin SpeechRecognizer bridged as window.Android.startSpeech().
  if (window.Android && typeof window.Android.startSpeech === 'function') {
    window.startSpeechFromJs = () => { window.Android.startSpeech(); };
  }
}

// Called by the Android bridge with the recognized text (or '' on cancel).
window.onSpeechResult = (text) => {
  if (text) {
    const el = $('input');
    el.value = el.value.trimEnd() + (el.value ? ' ' : '') + text;
    el.style.height = 'auto';
    el.style.height = Math.min(el.scrollHeight, 120) + 'px';
    toast('🎤 ' + text);
  }
  stopRecording();
};

function startRecording() {
  if (recording) { stopRecording(); return; }
  if (window.Android && typeof window.Android.startSpeech === 'function') {
    recording = true;
    $('mic-btn').classList.add('recording');
    window.Android.startSpeech();
    return;
  }
  if (!recognition) {
    toast('当前环境不支持语音输入');
    return;
  }
  recording = true;
  $('mic-btn').classList.add('recording');
  try {
    recognition.start();
  } catch (e) {
    stopRecording();
  }
}

function stopRecording() {
  recording = false;
  const b = $('mic-btn');
  if (b) b.classList.remove('recording');
  try { if (recognition && recognition.state !== 'inactive') recognition.stop(); } catch {}
}

$('mic-btn').onclick = startRecording;
initSpeech();

/* ---------- Bottom inset (browser nav bar / keyboard) ---------- */
const _composer = document.querySelector('.composer');
const _events = document.querySelector('.events');
const _approval = document.querySelector('#approval');

function fitComposer() {
  if (!_composer) return;
  const vv = window.visualViewport;
  let inset = 0;
  if (vv) inset = Math.max(0, window.innerHeight - vv.height - (vv.offsetTop || 0));
  _composer.style.bottom = inset + 'px';
  const h = _composer.offsetHeight;
  if (_events) _events.style.paddingBottom = (h + 8) + 'px';
  if (_approval) _approval.style.bottom = _approval.classList.contains('hidden') ? '' : (h + 'px');
}

window.addEventListener('resize', fitComposer);
window.addEventListener('orientationchange', fitComposer);
if (window.visualViewport) {
  window.visualViewport.addEventListener('resize', fitComposer);
  window.visualViewport.addEventListener('scroll', fitComposer);
}
window.addEventListener('load', fitComposer);
fitComposer();

/* ---------- Toast ---------- */

let toastTimer = null;
function toast(msg, ms) {
  const t = $('toast');
  t.textContent = msg;
  t.classList.remove('hidden');
  if (toastTimer) clearTimeout(toastTimer);
  toastTimer = setTimeout(() => t.classList.add('hidden'), ms || 2200);
}

/* ---------- Android bridge helpers (no-op in browser) ---------- */

function vibrate(ms) { try { if (window.Android) window.Android.vibrate(ms || 30); } catch {} }
function copyText(t) { try { if (window.Android) window.Android.copyToClipboard(t); else navigator.clipboard.writeText(t); } catch {} }

// hook copy on long-press of bubbles (best-effort)
document.addEventListener('dblclick', (e) => {
  const sel = window.getSelection().toString();
  if (sel) copyText(sel);
});

/* ---------- Login (web auth) ---------- */

const loginOverlay = $('login-overlay');
const loginForm = $('login-form');
const loginInput = $('login-password');
const loginSubmit = $('login-submit');
const loginError = $('login-error');

function showLogin() {
  if (loginOverlay) loginOverlay.classList.remove('hidden');
  if (loginInput) { loginInput.value = ''; loginInput.focus(); }
}
function hideLogin() {
  if (loginOverlay) loginOverlay.classList.add('hidden');
  if (loginError) loginError.classList.add('hidden');
}
async function doLogin(e) {
  if (e) e.preventDefault();
  const pw = loginInput ? loginInput.value : '';
  if (!pw || !loginSubmit) return;
  // The server password is exactly the configured web_password.
  loginSubmit.disabled = true;
  if (loginError) loginError.classList.add('hidden');
  try {
    const resp = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password: pw })
    });
    const data = await resp.json().catch(function () { return {}; });
    if (data && data.ok && data.token) {
      sessionToken = data.token;
      localStorage.setItem('mclaw_token', sessionToken);
      sessionStorage.setItem('mclaw_session', sessionToken);
      authRequired = true;
      hideLogin();
      connect();
    } else if (loginError) {
      loginError.classList.remove('hidden');
    }
  } catch (err) {
    if (loginError) {
      loginError.textContent = '登录请求失败，请重试';
      loginError.classList.remove('hidden');
    }
  } finally {
    loginSubmit.disabled = false;
  }
}
if (loginForm) loginForm.addEventListener('submit', doLogin);

// Logout: clear the saved token so the login screen shows again.
const logoutBtn = $('logout-btn');
if (logoutBtn) logoutBtn.onclick = () => {
  sessionToken = null;
  localStorage.removeItem('mclaw_token');
  sessionStorage.removeItem('mclaw_session');
  if (ws) { try { ws.close(); } catch (e) {} }
  setConnected(false);
  closeDrawer && closeDrawer();
  showLogin();
  toast('已退出登录，凭证已清除');
};

// On load, ask whether a login is required before connecting.
async function bootstrap() {
  try {
    const resp = await fetch('/api/auth-status');
    const data = await resp.json().catch(function () { return { required: false }; });
    if (data && data.required) {
      authRequired = true;
      if (sessionToken) {
        connect();
      } else {
        showLogin();
      }
      return;
    }
  } catch (e) {
    // Endpoint unavailable — fall back to a direct connection attempt.
  }
  connect();
}

/* ---------- Boot ---------- */

window.clearChat = () => { $('events').innerHTML = ''; currentAssistant = null; currentReasoning = null; currentToolCard = null; };
window.checkReconnect = () => { if (!connected) connect(); };

bootstrap();

/* ---- PWA: register service worker (Android 添加到主屏幕需要) ---- */
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').catch(function(e) {
      console.warn('SW register failed:', e);
    });
  });
}
