'use strict';
(function() {
var LI = {};

// ── State ──
var _state = {
  isRecording: false,
  isPaused: false,
  currentMode: null,      // 'dictation' | 'meeting' | 'email'
  currentRecordingId: null,
  lastUsedMode: 'dictation',
  mediaStream: null,
  mediaRecorder: null,
  audioContext: null,
  analyserNode: null,
  chunkIndex: 0,
  startTime: null,
  // Dictation
  speechRecognition: null,
  dictationTarget: null,  // the input/textarea element being dictated into
  dictationCallback: null,
  interimText: '',
  srRestartCount: 0,      // silent SR restart counter
  srGotResult: false,     // whether SR produced any result since last start
  levelAnimFrame: null,   // audio level meter animation frame
  micDeviceName: null,    // selected mic device label
  // Waveform
  waveformCanvas: null,
  waveformAnimFrame: null,
  // Timer
  timerInterval: null,
  // Mode picker
  modePickerVisible: false,
  longPressTimer: null
};

// ── API helpers ──
// Always use the CTM proxy (same origin) to avoid dual-brain DB inconsistency.
// The standalone Wall-E process may have a different brain DB connection,
// so we route through CTM which shares brain state with the Listening tab's
// api() calls in walle.js.

function _apiPost(path, body) {
  var token = window._ctmState?.token || '';
  return fetch('/api/wall-e' + path + '?token=' + token, {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(body)
  }).then(function(r) { return r.json(); });
}

function _apiUpload(path, recordingId, blob) {
  var token = window._ctmState?.token || '';
  var formData = new FormData();
  formData.append('recordingId', recordingId);
  formData.append('audio', blob, 'chunk.webm');
  return fetch('/api/wall-e' + path + '?token=' + token, {
    method: 'POST',
    body: formData
  }).then(function(r) { return r.json(); });
}

// ── Find the best dictation target ──
// Returns the currently focused input/textarea, or the most relevant one for the current tab.
// _preMicTarget is captured in onMicDown before the click steals focus.

function _isExcludedTarget(el) {
  if (!el) return true;
  // xterm's hidden textarea — writing .value does nothing, xterm uses keyboard events
  if (el.classList && el.classList.contains('xterm-helper-textarea')) return true;
  return false;
}

// Virtual target for terminal dictation (lightweight proxy)
var _terminalProxy = {
  isTerminalProxy: true,
  tagName: 'TERMINAL',
  id: 'li-terminal-proxy',
  classList: { add: function(){}, remove: function(){}, contains: function(){ return false; } },
  offsetParent: true,
  readOnly: false,
  isContentEditable: false,
  parentElement: null // set dynamically in _showDictationFeedback
};

function _isTerminalFocused() {
  var active = document.activeElement;
  return active && active.classList && active.classList.contains('xterm-helper-textarea');
}

function _findDictationTarget() {
  // 1. If an input/textarea is currently focused (not xterm), use it
  var active = document.activeElement;
  if (active && !_isExcludedTarget(active)) {
    if (active.tagName === 'TEXTAREA' || (active.tagName === 'INPUT' && active.type === 'text')) {
      if (active.offsetParent !== null && !active.readOnly) return active;
    }
    // Support contentEditable elements (e.g. pe-editor on Prompts tab)
    if (active.isContentEditable && active.offsetParent !== null) return active;
  }

  // 2. Pre-mic target (captured in onMicDown before click moved focus)
  var pre = _state._preMicTarget;
  if (pre && !_isExcludedTarget(pre) && pre.offsetParent !== null && !pre.readOnly) return pre;

  // 3. Look for the most relevant visible input based on current tab
  var candidates = [
    document.getElementById('walle-chat-input'),   // Wall-E chat tab
    document.getElementById('pe-editor'),           // Prompt editor (contentEditable)
    document.getElementById('pe-tpl-content'),      // Prompt template editor
  ];

  // Also include active session inputs (.walle-input textareas)
  var sessionInputs = document.querySelectorAll('.walle-input');
  for (var j = 0; j < sessionInputs.length; j++) {
    candidates.push(sessionInputs[j]);
  }

  for (var i = 0; i < candidates.length; i++) {
    var el = candidates[i];
    if (el && el.offsetParent !== null) return el;
  }

  // 4. Terminal fallback: if xterm is focused or we're on Sessions tab with an active terminal
  if (_isTerminalFocused() || _hasActiveTerminal()) {
    return _terminalProxy;
  }

  return null;
}

function _hasActiveTerminal() {
  var hash = window.location.hash || '';
  var onSessionsTab = hash.indexOf('session') !== -1 || hash === '' || hash === '#';
  return onSessionsTab && typeof window._ctmGetActiveTerminal === 'function' && window._ctmGetActiveTerminal() !== null;
}

// Creates a real textarea overlay inside the terminal for dictation input.
// TUI apps (Claude Code) use raw mode — direct PTY writes don't echo reliably.
// This textarea collects dictated text; on Enter or dictation stop it's sent to PTY.
function _createTerminalDictationInput() {
  // Reuse existing if present
  var existing = document.getElementById('li-terminal-dictation');
  if (existing) {
    existing.value = '';
    existing.style.display = '';
    existing.focus();
    return existing;
  }

  var termInfo = window._ctmGetActiveTerminal ? window._ctmGetActiveTerminal() : null;
  if (!termInfo) return _terminalProxy; // fallback

  // Find the active terminal container
  var container = null;
  var all = document.querySelectorAll('.term-container');
  for (var i = 0; i < all.length; i++) {
    if (all[i].offsetParent !== null && all[i].classList.contains('active')) { container = all[i]; break; }
  }
  if (!container) {
    for (var i = 0; i < all.length; i++) {
      if (all[i].offsetParent !== null) { container = all[i]; break; }
    }
  }
  if (!container) return _terminalProxy; // fallback

  var textarea = document.createElement('textarea');
  textarea.id = 'li-terminal-dictation';
  textarea.className = 'li-terminal-dictation';
  textarea.placeholder = 'Dictating... press Enter to send to terminal';
  textarea.rows = 2;
  textarea.addEventListener('keydown', function(e) {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      _submitTerminalDictation();
    }
    if (e.key === 'Escape') {
      e.preventDefault();
      // Clear and hide without sending
      textarea.value = '';
      LI.stopRecording();
    }
  });
  // Auto-resize
  textarea.addEventListener('input', function() {
    textarea.style.height = 'auto';
    textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
  });

  container.appendChild(textarea);
  textarea.focus();
  return textarea;
}

function _submitTerminalDictation() {
  var textarea = document.getElementById('li-terminal-dictation');
  if (!textarea) return;
  var text = textarea.value.trim();
  if (text && window._ctmSendToTerminal) {
    // Send text + newline to terminal (newline submits the prompt)
    window._ctmSendToTerminal(text + '\n');
  }
  textarea.value = '';
  textarea.style.height = '';
}

// ── Text get/set helpers (textarea + contentEditable) ──

function _getTargetText(el) {
  if (!el) return '';
  if (el.isTerminalProxy) return ''; // Terminal has no readable "value"
  if (el.isContentEditable) return el.innerText || '';
  return el.value || '';
}

function _setTargetText(el, text) {
  if (!el) return;
  if (el.isTerminalProxy) return; // Terminal text sent via _ctmSendToTerminal
  if (el.isContentEditable) {
    el.innerText = text;
  } else {
    el.value = text;
  }
  el.dispatchEvent(new Event('input', { bubbles: true }));
}

function _scrollTargetToEnd(el) {
  if (!el) return;
  if (el.isTerminalProxy) return;
  if (el.isContentEditable) {
    var range = document.createRange();
    var sel = window.getSelection();
    range.selectNodeContents(el);
    range.collapse(false);
    sel.removeAllRanges();
    sel.addRange(range);
    el.scrollTop = el.scrollHeight;
  } else {
    el.scrollTop = el.scrollHeight;
  }
}

// ── Smart Mic Selection ──
// Enumerate audio devices and pick the best mic.
// Priority: saved preference > built-in > "Default" label > first available.
// Skips iPhone Continuity mics and virtual devices (Teams, Zoom).

function _getSavedLang() {
  var saved = null;
  try { saved = localStorage.getItem('li-preferred-lang'); } catch(e) {}
  if (!saved || saved === 'auto') return navigator.language || 'en-US';
  return saved;
}

var _langNames = {
  'en': 'English', 'zh': '\u4E2D\u6587', 'ja': '\u65E5\u672C\u8A9E',
  'ko': '\uD55C\uAD6D\uC5B4', 'es': 'Espa\u00F1ol', 'fr': 'Fran\u00E7ais',
  'de': 'Deutsch', 'pt': 'Portugu\u00EAs'
};

var _langCodeMap = {
  'en': 'en-US', 'zh': 'zh-CN', 'ja': 'ja-JP', 'ko': 'ko-KR',
  'es': 'es-ES', 'fr': 'fr-FR', 'de': 'de-DE', 'pt': 'pt-BR'
};

function _tryDetectLanguage(text) {
  if (!('LanguageDetector' in self)) return;
  if (!text || text.length < 10) return;

  var currentLang = _getSavedLang().substring(0, 2);

  (function() {
    var p = _state._langDetector
      ? Promise.resolve(_state._langDetector)
      : self.LanguageDetector.create().then(function(d) { _state._langDetector = d; return d; });

    p.then(function(detector) {
      return detector.detect(text);
    }).then(function(results) {
      if (!results || !results.length) return;
      var top = results[0];
      if (top.confidence > 0.8 && top.detectedLanguage !== currentLang) {
        var name = _langNames[top.detectedLanguage] || top.detectedLanguage;
        var switchCode = _langCodeMap[top.detectedLanguage];
        if (!switchCode) return;
        var toast = document.createElement('div');
        toast.id = 'li-toast';
        toast.className = 'li-toast';
        toast.innerHTML = 'Detected ' + _escText(name) + ' \u2014 <a href="#" style="color:#7aa2f7;text-decoration:underline">switch?</a>';
        toast.querySelector('a').addEventListener('click', function(e) {
          e.preventDefault();
          try { localStorage.setItem('li-preferred-lang', switchCode); } catch(ex) {}
          _showToast('Language: ' + name);
          if (_state.speechRecognition) {
            try { _state.speechRecognition.lang = switchCode; } catch(ex) {}
          }
        });
        var existing = document.getElementById('li-toast');
        if (existing) existing.remove();
        document.body.appendChild(toast);
        setTimeout(function() { if (document.body.contains(toast)) toast.remove(); }, 5000);
      }
    }).catch(function() {});
  })();
}

// ── Voice Commands ──
// Exact-match spoken commands intercepted before text insertion.
// Only checked on isFinal SR results to prevent premature execution.

var _VOICE_COMMANDS = {
  en: [
    { triggers: ['send', 'submit', 'enter'], action: 'submit' },
    { triggers: ['stop', 'stop listening', 'stop dictation'], action: 'stop' },
    { triggers: ['clear', 'clear all'], action: 'clear' },
    { triggers: ['undo', 'undo that'], action: 'undo' },
    { triggers: ['cancel'], action: 'cancel' },
    { triggers: ['new line'], action: 'newline' },
    { triggers: ['new paragraph'], action: 'paragraph' },
    { triggers: ['switch to sessions', 'go to sessions'], action: 'nav:sessions' },
    { triggers: ['switch to wall e', 'switch to wally'], action: 'nav:walle' },
    { triggers: ['switch to prompts'], action: 'nav:prompts' },
    { triggers: ['switch to setup', 'switch to settings'], action: 'nav:setup' },
  ],
  zh: [
    { triggers: ['发送', '提交'], action: 'submit' },
    { triggers: ['停止', '停止听写', '停止录音'], action: 'stop' },
    { triggers: ['清除', '清空'], action: 'clear' },
    { triggers: ['撤销', '撤回'], action: 'undo' },
    { triggers: ['取消'], action: 'cancel' },
    { triggers: ['换行'], action: 'newline' },
    { triggers: ['新段落'], action: 'paragraph' },
    { triggers: ['切换到会话', '切换到终端'], action: 'nav:sessions' },
    { triggers: ['切换到助手'], action: 'nav:walle' },
    { triggers: ['切换到提示词'], action: 'nav:prompts' },
    { triggers: ['切换到设置'], action: 'nav:setup' },
  ],
  ja: [
    { triggers: ['送信', '送って'], action: 'submit' },
    { triggers: ['停止', '止めて'], action: 'stop' },
    { triggers: ['クリア', '消して'], action: 'clear' },
    { triggers: ['取り消し'], action: 'undo' },
    { triggers: ['キャンセル'], action: 'cancel' },
    { triggers: ['改行'], action: 'newline' },
  ],
  ko: [
    { triggers: ['전송', '보내'], action: 'submit' },
    { triggers: ['중지', '그만'], action: 'stop' },
    { triggers: ['지우기', '클리어'], action: 'clear' },
    { triggers: ['취소'], action: 'cancel' },
  ]
};

function _tryVoiceCommand(text, target) {
  var normalized = text.trim().toLowerCase();
  if (!normalized) return false;

  var lang = _getSavedLang();
  var langPrefix = lang.substring(0, 2);
  var commandSets = [_VOICE_COMMANDS[langPrefix], _VOICE_COMMANDS.en].filter(Boolean);

  for (var s = 0; s < commandSets.length; s++) {
    var cmds = commandSets[s];
    for (var c = 0; c < cmds.length; c++) {
      var cmd = cmds[c];
      for (var t = 0; t < cmd.triggers.length; t++) {
        if (normalized === cmd.triggers[t]) {
          _executeVoiceCommand(cmd.action, target);
          return true;
        }
      }
    }
  }
  return false;
}

function _executeVoiceCommand(action, target) {
  switch (action) {
    case 'submit':
      if (_state._isTerminalDictation) {
        _submitTerminalDictation();
      } else if (target && !target.isTerminalProxy) {
        var form = target.closest ? target.closest('form') : null;
        if (form) {
          form.dispatchEvent(new Event('submit', { bubbles: true }));
        } else {
          target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
        }
      }
      _showToast('Sent');
      break;
    case 'stop':
      LI.stopRecording();
      break;
    case 'clear':
      if (target && !target.isTerminalProxy) {
        _state._prevCommittedLen = _state._committedLen;
        _setTargetText(target, '');
        _state._committedLen = 0;
      }
      _showToast('Cleared');
      break;
    case 'undo':
      if (target && !target.isTerminalProxy && _state._prevCommittedLen !== undefined) {
        var cur = _getTargetText(target);
        _setTargetText(target, cur.substring(0, _state._prevCommittedLen));
        _state._committedLen = _state._prevCommittedLen;
      }
      _showToast('Undone');
      break;
    case 'cancel':
      if (target && !target.isTerminalProxy) {
        _setTargetText(target, _getTargetText(target).substring(0, _state._initialLen || 0));
      }
      LI.stopRecording();
      _showToast('Cancelled');
      break;
    case 'newline':
      if (target && !target.isTerminalProxy) {
        _setTargetText(target, _getTargetText(target) + '\n');
        _state._committedLen = _getTargetText(target).length;
      }
      break;
    case 'paragraph':
      if (target && !target.isTerminalProxy) {
        _setTargetText(target, _getTargetText(target) + '\n\n');
        _state._committedLen = _getTargetText(target).length;
      }
      break;
    default:
      if (action.indexOf('nav:') === 0) {
        var dest = action.substring(4);
        if (typeof window.navTo === 'function') window.navTo(dest);
        _showToast('Switched to ' + dest);
      }
      break;
  }
}

// ── LLM Refinement (punctuation + cleanup) ──
// Debounced: collects SR final results, sends batch to server after 1.5s pause.
// Server calls LLM to add punctuation and fix capitalization. Replaces raw text.

var _refineQueue = [];
var _refineTimer = null;

function _requestRefinement(text, target) {
  _refineQueue.push(text);
  clearTimeout(_refineTimer);
  _refineTimer = setTimeout(function() {
    var batch = _refineQueue.join(' ');
    _refineQueue = [];
    if (!batch.trim()) return;

    var lang = _getSavedLang();
    _apiPost('/listening/refine', { text: batch, lang: lang, context: _state.currentMode })
      .then(function(result) {
        if (!result || !result.data || !result.data.refined) return;
        var refined = result.data.refined;
        if (refined === batch) return; // no change

        if (!target || !document.contains(target)) return;
        if (target.isTerminalProxy) return; // don't refine terminal text

        // Replace raw text with refined text
        var curText = _getTargetText(target);
        var base = curText.substring(0, _state._refineBaseLen || 0);
        _setTargetText(target, base + refined);
        _state._committedLen = _getTargetText(target).length;
        _scrollTargetToEnd(target);
      })
      .catch(function() {}); // silent — raw text is still there
  }, 1500);
}

async function _selectMic() {
  try {
    // Step 1: Enumerate devices. Labels are only available if mic permission was
    // previously granted. We avoid a temp getUserMedia({ audio: true }) because
    // it would briefly activate the browser's default mic (often the iPhone via
    // macOS Continuity), causing an "Audio Disconnected" notification.
    var devices = await navigator.mediaDevices.enumerateDevices();
    var inputs = devices.filter(function(d) { return d.kind === 'audioinput'; });
    var hasLabels = inputs.some(function(d) { return d.label && d.label.length > 0; });

    if (!hasLabels && inputs.length > 0) {
      // Check if permission is already granted (labels should then be available after re-enumerate)
      try {
        var perm = await navigator.permissions.query({ name: 'microphone' });
        if (perm.state === 'granted') {
          devices = await navigator.mediaDevices.enumerateDevices();
          inputs = devices.filter(function(d) { return d.kind === 'audioinput'; });
          hasLabels = inputs.some(function(d) { return d.label && d.label.length > 0; });
        }
      } catch(e) { /* permissions API not available */ }
    }

    if (!hasLabels && inputs.length > 0) {
      // Still no labels — must trigger getUserMedia for permission.
      // Use deviceId 'default' to prefer the OS system default mic
      // (usually MacBook Pro) instead of Chrome's default (often iPhone).
      var tempStream = await navigator.mediaDevices.getUserMedia({
        audio: { deviceId: { ideal: 'default' } }
      });
      tempStream.getTracks().forEach(function(t) { t.stop(); });
      devices = await navigator.mediaDevices.enumerateDevices();
      inputs = devices.filter(function(d) { return d.kind === 'audioinput'; });
    }

    if (inputs.length === 0) return undefined;

    console.log('[LI] Audio inputs:', inputs.map(function(d) { return d.label + ' (' + d.deviceId.substring(0, 8) + ')'; }).join(', '));

    // 2. Saved preference
    var saved = null;
    try { saved = localStorage.getItem('li-preferred-mic'); } catch(e) {}
    if (saved) {
      var match = inputs.find(function(d) { return d.deviceId === saved; });
      if (match) {
        _state.micDeviceName = match.label || 'Saved mic';
        return match.deviceId;
      }
    }

    // 3. Prefer built-in mic (skip iPhone, virtual devices)
    var builtIn = inputs.find(function(d) {
      var label = d.label || '';
      return (label.includes('Built-in') || (label.includes('MacBook') && !label.includes('iPhone')))
        && !label.includes('Virtual');
    });
    if (builtIn) {
      _state.micDeviceName = builtIn.label;
      return builtIn.deviceId;
    }

    // 4. Fall back to "Default" labeled device (but not if it's the iPhone)
    var def = inputs.find(function(d) {
      var label = d.label || '';
      return label.includes('Default') && !label.includes('iPhone');
    });
    if (def) {
      _state.micDeviceName = def.label;
      return def.deviceId;
    }

    // 5. First non-virtual, non-iPhone device
    var nonVirtual = inputs.find(function(d) {
      var label = d.label || '';
      return !label.includes('iPhone') && !label.includes('Virtual') && !label.includes('ZoomAudio') && !label.includes('Teams Audio');
    });
    if (nonVirtual) {
      _state.micDeviceName = nonVirtual.label;
      return nonVirtual.deviceId;
    }

    // 6. Anything (last resort)
    _state.micDeviceName = inputs[0].label || 'Unknown mic';
    return inputs[0].deviceId;
  } catch(err) {
    console.warn('[LI] Device enumeration failed, using default mic:', err);
    return undefined;
  }
}

// ── Recording controls ──

LI.startRecording = function(mode, opts) {
  opts = opts || {};
  if (_state.isRecording) return Promise.reject(new Error('Already recording'));

  _state.currentMode = mode || 'dictation';
  _state.lastUsedMode = _state.currentMode;
  try { localStorage.setItem('li-last-mode', _state.currentMode); } catch(e) {}

  // For dictation mode, use Web Speech API + server recording for transcript persistence
  if (_state.currentMode === 'dictation' || _state.currentMode === 'dictation-chat' || _state.currentMode === 'dictation-prompt') {
    var target = opts.target || _findDictationTarget();
    if (!target) {
      return Promise.reject(new Error('No input field found. Focus an input first.'));
    }
    return _startDictation(target, opts.onText);
  }

  // For meeting/email modes, full recording pipeline
  return _selectMic().then(function(micId) {
    var constraints = micId ? { audio: { deviceId: { exact: micId } } } : { audio: true };
    return navigator.mediaDevices.getUserMedia(constraints);
  })
    .then(function(stream) {
      _state.mediaStream = stream;

      // Start server-side recording session
      return _apiPost('/listening/start', {
        mode: _state.currentMode,
        title: opts.title || ''
      });
    })
    .then(function(result) {
      if (result.error) throw new Error(result.error);
      _state.currentRecordingId = result.data.id;
      _state.isRecording = true;
      _state.isPaused = false;
      _state.chunkIndex = 0;
      _state.startTime = Date.now();

      // Set up audio context + analyser for waveform
      _state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
      var source = _state.audioContext.createMediaStreamSource(_state.mediaStream);
      _state.analyserNode = _state.audioContext.createAnalyser();
      _state.analyserNode.fftSize = 256;
      source.connect(_state.analyserNode);

      // Set up MediaRecorder for chunked upload
      var mimeType = typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
        ? 'audio/webm;codecs=opus'
        : 'audio/webm';
      _state.mediaRecorder = new MediaRecorder(_state.mediaStream, { mimeType: mimeType });

      _state._lastUploadPromise = Promise.resolve();
      _state.mediaRecorder.ondataavailable = function(e) {
        if (e.data && e.data.size > 0 && _state.currentRecordingId) {
          _state.chunkIndex++;
          _state._lastUploadPromise = _apiUpload('/listening/upload', _state.currentRecordingId, e.data)
            .catch(function(err) { console.error('[LI] Chunk upload failed:', err); });
        }
      };

      _state.mediaRecorder.start(10000); // 10-second chunks

      // Start Web Speech for live preview (if available)
      _startLiveSpeech();

      // Start timer
      _startTimer();

      // Update UI
      _updateGlobalMicUI();

      return { recordingId: _state.currentRecordingId };
    })
    .catch(function(err) {
      _cleanup();
      throw err;
    });
};

LI.stopRecording = function() {
  if (!_state.isRecording) return Promise.resolve();

  // Stop dictation mode
  if (_state.currentMode === 'dictation' || _state.currentMode === 'dictation-chat' || _state.currentMode === 'dictation-prompt') {
    return _stopDictation();
  }

  var recordingId = _state.currentRecordingId;
  var recorder = _state.mediaRecorder;

  // Stop live speech
  _stopLiveSpeech();

  // Stop timer
  _stopTimer();

  // Stop waveform
  if (_state.waveformAnimFrame) {
    cancelAnimationFrame(_state.waveformAnimFrame);
    _state.waveformAnimFrame = null;
  }

  _state.isRecording = false;
  _state.isPaused = false;
  _state.currentRecordingId = null;
  _updateGlobalMicUI();

  // Wait for MediaRecorder to finish (fires final ondataavailable + onstop)
  // then wait for the last chunk upload, then tell the server to finalize
  var recorderDone = new Promise(function(resolve) {
    if (recorder && recorder.state !== 'inactive') {
      recorder.onstop = function() { resolve(); };
      recorder.stop();
    } else {
      resolve();
    }
  });

  return recorderDone
    .then(function() {
      // Wait for the final chunk upload to complete
      return _state._lastUploadPromise || Promise.resolve();
    })
    .then(function() {
      // Now safe to tell server to finalize
      if (recordingId) {
        return _apiPost('/listening/stop', { recordingId: recordingId });
      }
      return { ok: true };
    })
    .then(function(result) {
      // Cleanup media resources after server confirms
      if (_state.mediaStream) {
        _state.mediaStream.getTracks().forEach(function(t) { t.stop(); });
      }
      if (_state.audioContext) {
        _state.audioContext.close().catch(function() {});
      }
      _state.mediaRecorder = null;
      _state.mediaStream = null;
      _state.audioContext = null;
      _state.analyserNode = null;
      _state._lastUploadPromise = null;
      return result;
    });
};

LI.toggleRecording = function() {
  if (_state.isRecording) {
    return LI.stopRecording();
  } else {
    return LI.startRecording('dictation');
  }
};

// ── Dictation (Web Speech only) ──

function _hasSpeechRecognition() {
  return !!(window.SpeechRecognition || window.webkitSpeechRecognition);
}

function _startDictation(target, onText) {
  // For terminal targets, create a real textarea overlay instead of sending directly to PTY.
  // TUI programs (Claude Code, etc.) set raw mode — direct PTY writes don't echo reliably.
  _state._isTerminalDictation = false;
  if (target && target.isTerminalProxy) {
    _state._isTerminalDictation = true;
    target = _createTerminalDictationInput();
  }

  _state.dictationTarget = target;
  _state.dictationCallback = onText || null;
  _state.interimText = '';
  _state.srRestartCount = 0;
  _state.srGotResult = false;
  _state._committedLen = _getTargetText(target).length; // track where stable text ends
  _state._prevCommittedLen = _state._committedLen;
  _state._initialLen = _state._committedLen;
  _state._refineBaseLen = _state._committedLen;

  // getUserMedia-based dictation: captures audio regardless of SR working
  return _selectMic().then(function(micId) {
    var constraints = micId ? { audio: { deviceId: { exact: micId } } } : { audio: true };
    return navigator.mediaDevices.getUserMedia(constraints);
  }).then(function(stream) {
    _state.mediaStream = stream;
    _state.isRecording = true;
    _state.startTime = Date.now();
    _state.chunkIndex = 0;

    // Create server-side recording
    return _apiPost('/listening/start', {
      mode: _state.currentMode || 'dictation',
      title: 'Dictation ' + new Date().toLocaleTimeString()
    }).then(function(result) {
      if (result.error) throw new Error(result.error);
      _state.currentRecordingId = result.data.id;
    }).catch(function(err) {
      console.error('[LI] Failed to create server recording for dictation:', err);
    });
  }).then(function() {
    // Set up AudioContext + AnalyserNode for live level meter
    _state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
    var source = _state.audioContext.createMediaStreamSource(_state.mediaStream);
    _state.analyserNode = _state.audioContext.createAnalyser();
    _state.analyserNode.fftSize = 256;
    source.connect(_state.analyserNode);

    // Set up MediaRecorder for chunked upload (2s chunks for dictation responsiveness)
    var mimeType = typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
      ? 'audio/webm;codecs=opus'
      : 'audio/webm';
    _state.mediaRecorder = new MediaRecorder(_state.mediaStream, { mimeType: mimeType });

    _state._lastUploadPromise = Promise.resolve();
    _state.mediaRecorder.ondataavailable = function(e) {
      if (e.data && e.data.size > 0 && _state.currentRecordingId) {
        _state.chunkIndex++;
        _state._lastUploadPromise = _apiUpload('/listening/upload', _state.currentRecordingId, e.data)
          .catch(function(err) { console.error('[LI] Dictation chunk upload failed:', err); });
      }
    };

    _state.mediaRecorder.start(2000); // 2-second chunks

    // Show visual feedback with level meter
    _showDictationFeedback(target);
    _startLevelMeter();

    // Start SpeechRecognition in parallel (best-effort for real-time text)
    _startDictationSR(target);

    _startTimer();
    _updateGlobalMicUI();

    return { mode: _state.currentMode, recordingId: _state.currentRecordingId };
  }).catch(function(err) {
    _cleanup();
    throw err;
  });
}

// Start SpeechRecognition for dictation (best-effort parallel to getUserMedia)
function _startDictationSR(target) {
  if (!_hasSpeechRecognition()) return;
  _state.srGotResult = false;
  _launchSR(target);
}

// Creates a FRESH SpeechRecognition instance each time.
// Chrome's SR often locks up after "network" errors — reusing the same instance doesn't help.
function _launchSR(target) {
  if (!_state.isRecording) return;

  var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  var recognition = new SpeechRecognition();
  recognition.continuous = true;
  recognition.interimResults = true;
  recognition.lang = _getSavedLang();

  _state.speechRecognition = recognition;

  recognition.onresult = function(event) {
    _state.srGotResult = true;
    _state.srRestartCount = 0; // reset counter on success

    // Defensive: re-resolve target if DOM was rebuilt (e.g., renderChatUI() via innerHTML)
    if (target && target.id && !document.contains(target)) {
      var fresh = document.getElementById(target.id);
      if (fresh) {
        target = fresh;
        _state.dictationTarget = fresh;
      }
    }

    var final = '';
    var interim = '';
    for (var i = event.resultIndex; i < event.results.length; i++) {
      var transcript = event.results[i][0].transcript;
      if (event.results[i].isFinal) {
        final += transcript;
      } else {
        interim = transcript;
      }
    }

    if (final && target) {
      // Voice command check — exact match intercepts before text insertion
      if (_tryVoiceCommand(final, target)) return;

      _state.interimText = '';
      _state._prevCommittedLen = _state._committedLen;

      if (target.isTerminalProxy) {
        // Terminal: send final text directly to PTY, show nothing interim
        if (window._ctmSendToTerminal) window._ctmSendToTerminal(final);
        _updateTerminalPreview('');
      } else {
        // Textarea/contentEditable: committed-length tracking
        _updateInterimPreview(target, '');
        var curText = _getTargetText(target);
        var baseVal = curText.substring(0, _state._committedLen || 0);
        var needsSpace = baseVal && !baseVal.endsWith(' ') && !baseVal.endsWith('\n')
          && !final.startsWith(' ');
        _setTargetText(target, baseVal + (needsSpace ? ' ' : '') + final);
        _state._committedLen = _getTargetText(target).length;
        _scrollTargetToEnd(target);
      }

      if (_state.currentRecordingId) {
        _apiPost('/listening/interim', {
          recordingId: _state.currentRecordingId,
          text: final,
          isFinal: true,
          timestamp: Date.now() - _state.startTime
        }).catch(function() {});
      }
      _tryDetectLanguage(final);
      // Async LLM refinement (adds punctuation, fixes capitalization)
      if (!target.isTerminalProxy) _requestRefinement(final, target);
    } else if (interim && target) {
      _state.interimText = interim;

      if (target.isTerminalProxy) {
        // Terminal: only show interim in overlay, don't send to PTY
        _updateTerminalPreview(interim);
      } else {
        _updateInterimPreview(target, interim);
        var curText = _getTargetText(target);
        var baseVal = curText.substring(0, _state._committedLen || 0);
        var needsSpace = baseVal && !baseVal.endsWith(' ') && !baseVal.endsWith('\n')
          && !interim.startsWith(' ');
        _setTargetText(target, baseVal + (needsSpace ? ' ' : '') + interim);
        _scrollTargetToEnd(target);
      }
    }

    if (_state.dictationCallback) {
      _state.dictationCallback(final, interim);
    }
  };

  recognition.onerror = function(event) {
    if (event.error === 'no-speech') return;
    if (event.error === 'aborted') return;
    console.warn('[LI] Speech recognition error:', event.error);
    if (event.error === 'not-allowed') {
      // SR denied but audio capture continues — just disable SR
      _state.speechRecognition = null;
    }
  };

  recognition.onend = function() {
    // Detach to prevent stale listeners on reused instances
    recognition.onresult = null;
    recognition.onerror = null;
    recognition.onend = null;

    if (!_state.isRecording) return;
    if (_state.currentMode !== 'dictation' && _state.currentMode !== 'dictation-chat' && _state.currentMode !== 'dictation-prompt') return;

    if (!_state.srGotResult) {
      _state.srRestartCount++;
      console.warn('[LI] SR ended with no results (attempt ' + (_state.srRestartCount + 1) + '/5)');
    }

    if (_state.srRestartCount >= 4) {
      console.warn('[LI] SR failed 5 times — stopping SR. Audio capture continues.');
      _showToast('Speech-to-text unavailable. Audio is still recording.', 5000);
      _state.speechRecognition = null;
      return;
    }

    // Delay before retry — fresh instance each time (Chrome SR locks up on reuse after errors)
    _state.srGotResult = false;
    var delay = _state.srRestartCount < 2 ? 500 : 2000;
    setTimeout(function() {
      if (_state.isRecording) _launchSR(target);
    }, delay);
  };

  try {
    recognition.start();
  } catch(e) {
    console.warn('[LI] SR start failed:', e);
  }
}

function _stopDictation() {
  // Defensive: re-resolve target if DOM was rebuilt (skip for terminal proxy)
  if (_state.dictationTarget && !_state.dictationTarget.isTerminalProxy &&
      _state.dictationTarget.id && !document.contains(_state.dictationTarget)) {
    var fresh = document.getElementById(_state.dictationTarget.id);
    if (fresh) _state.dictationTarget = fresh;
  }

  // If target is the terminal dictation textarea, auto-submit its content to PTY
  var termDictation = document.getElementById('li-terminal-dictation');
  if (termDictation && _state.dictationTarget === termDictation) {
    _submitTerminalDictation();
  }

  // Commit any remaining interim text before cleanup
  if (_state.dictationTarget && _state.interimText) {
    if (_state.dictationTarget.isTerminalProxy) {
      // Terminal: send remaining interim as final text to PTY
      if (window._ctmSendToTerminal) window._ctmSendToTerminal(_state.interimText);
      _updateTerminalPreview('');
    } else {
      var curText = _getTargetText(_state.dictationTarget);
      var baseVal = curText.substring(0, _state._committedLen || 0);
      var needsSpace = baseVal && !baseVal.endsWith(' ') && !baseVal.endsWith('\n')
        && !_state.interimText.startsWith(' ');
      _setTargetText(_state.dictationTarget, baseVal + (needsSpace ? ' ' : '') + _state.interimText);
    }
    _state.interimText = '';
  }

  // Remove visual feedback
  if (_state.dictationTarget) {
    _hideDictationFeedback(_state.dictationTarget);
    _updateInterimPreview(_state.dictationTarget, '');
  }

  // Clean up terminal dictation textarea
  if (termDictation) {
    termDictation.remove();
  }
  _state._isTerminalDictation = false;

  // Stop level meter animation
  _stopLevelMeter();

  // Flush pending refinement
  clearTimeout(_refineTimer);
  _refineQueue = [];

  // Stop SpeechRecognition
  if (_state.speechRecognition) {
    _state.speechRecognition.onend = null; // prevent auto-restart
    try { _state.speechRecognition.stop(); } catch(e) {}
    _state.speechRecognition = null;
  }

  var recordingId = _state.currentRecordingId;
  var recorder = _state.mediaRecorder;

  _state.isRecording = false;
  _state.currentRecordingId = null;
  _state.dictationTarget = null;
  _state.dictationCallback = null;
  _state.srRestartCount = 0;
  _state.srGotResult = false;
  _stopTimer();
  _updateGlobalMicUI();

  // Wait for MediaRecorder to flush final chunk, then finalize server-side
  var recorderDone = new Promise(function(resolve) {
    if (recorder && recorder.state !== 'inactive') {
      recorder.onstop = function() { resolve(); };
      recorder.stop();
    } else {
      resolve();
    }
  });

  return recorderDone
    .then(function() {
      return _state._lastUploadPromise || Promise.resolve();
    })
    .then(function() {
      if (recordingId) {
        return _apiPost('/listening/stop', { recordingId: recordingId });
      }
      return { ok: true };
    })
    .then(function(result) {
      // Cleanup media resources
      if (_state.mediaStream) {
        _state.mediaStream.getTracks().forEach(function(t) { t.stop(); });
        _state.mediaStream = null;
      }
      if (_state.audioContext) {
        _state.audioContext.close().catch(function() {});
        _state.audioContext = null;
      }
      _state.mediaRecorder = null;
      _state.analyserNode = null;
      _state._lastUploadPromise = null;
      return result;
    })
    .catch(function(err) {
      console.error('[LI] Failed to stop dictation cleanly:', err);
      // Cleanup even on error
      if (_state.mediaStream) {
        _state.mediaStream.getTracks().forEach(function(t) { t.stop(); });
        _state.mediaStream = null;
      }
      if (_state.audioContext) {
        _state.audioContext.close().catch(function() {});
        _state.audioContext = null;
      }
      _state.mediaRecorder = null;
      _state.analyserNode = null;
      _state._lastUploadPromise = null;
    });
}

// ── Dictation Visual Feedback ──

function _escText(s) {
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function _buildIndicatorHTML() {
  var micShort = _state.micDeviceName
    ? _state.micDeviceName.replace(/\s*\(Built-in\)\s*$/i, '').replace(/\s*Microphone\s*$/i, '').trim()
    : '';
  var micLabel = micShort ? ' \u00B7 ' + _escText(micShort) : '';
  return '<span class="li-rec-dot"></span>' +
    '<span class="li-dictation-label">Listening' + micLabel + '</span>' +
    '<span id="li-timer" class="li-rec-timer">0:00</span>';
}

function _showDictationFeedback(target) {
  if (!target) return;

  if (target.isTerminalProxy || _state._isTerminalDictation) {
    // Terminal mode: insert indicator bar at top of active terminal container
    var termInfo = window._ctmGetActiveTerminal ? window._ctmGetActiveTerminal() : null;
    if (!termInfo) return;
    var container = null;
    var all = document.querySelectorAll('.term-container');
    for (var i = 0; i < all.length; i++) {
      if (all[i].offsetParent !== null && all[i].classList.contains('active')) { container = all[i]; break; }
    }
    if (!container) {
      for (var i = 0; i < all.length; i++) {
        if (all[i].offsetParent !== null) { container = all[i]; break; }
      }
    }
    if (!container) return;
    var indicator = document.createElement('div');
    indicator.className = 'li-terminal-indicator';
    indicator.id = 'li-dictation-indicator';
    indicator.innerHTML = _buildIndicatorHTML() +
      '<span class="li-interim-text" id="li-terminal-interim"></span>';
    container.insertBefore(indicator, container.firstChild);
    return;
  }

  // Standard mode: indicator above the input
  target.classList.add('li-dictation-active');
  var indicator = document.createElement('div');
  indicator.className = 'li-dictation-indicator';
  indicator.id = 'li-dictation-indicator';
  indicator.innerHTML = _buildIndicatorHTML();
  var parent = target.parentElement;
  if (parent) {
    parent.style.position = parent.style.position || 'relative';
    parent.appendChild(indicator);
  }
}

function _hideDictationFeedback(target) {
  if (!target) return;
  if (!target.isTerminalProxy && !_state._isTerminalDictation) {
    target.classList.remove('li-dictation-active');
  }
  var indicator = document.getElementById('li-dictation-indicator');
  if (indicator) indicator.remove();
}

function _updateTerminalPreview(text) {
  var el = document.getElementById('li-terminal-interim');
  if (el) el.textContent = text ? '\u201C' + text + '\u2026\u201D' : '';
}

function _updateInterimPreview(target, text) {
  if (!target) return;
  var previewId = 'li-interim-preview';
  var existing = document.getElementById(previewId);

  if (!text) {
    if (existing) existing.remove();
    return;
  }

  if (!existing) {
    existing = document.createElement('div');
    existing.id = previewId;
    existing.className = 'li-interim-preview';
    var parent = target.parentElement;
    if (parent) parent.appendChild(existing);
  }
  existing.textContent = text;
}

// ── Audio Level Meter (dictation) ──

function _startLevelMeter() {
  if (!_state.analyserNode) return;
  var analyser = _state.analyserNode;
  var dataArray = new Uint8Array(analyser.frequencyBinCount);

  function tick() {
    if (!_state.isRecording || !_state.analyserNode) return;
    _state.levelAnimFrame = requestAnimationFrame(tick);

    analyser.getByteFrequencyData(dataArray);

    // Compute average level (0-255)
    var sum = 0;
    for (var i = 0; i < dataArray.length; i++) sum += dataArray[i];
    var avg = sum / dataArray.length;
    var pct = Math.min(100, Math.round((avg / 128) * 100));

    // Update the level bar in the dictation indicator
    var bar = document.getElementById('li-level-bar');
    if (!bar) {
      // Create level bar inside the dictation indicator
      var indicator = document.getElementById('li-dictation-indicator');
      if (indicator) {
        bar = document.createElement('span');
        bar.id = 'li-level-bar';
        bar.className = 'li-level-bar';
        indicator.appendChild(bar);
      }
    }
    if (bar) {
      bar.style.width = Math.max(4, pct) + '%';
      // Color: green < 50%, yellow 50-80%, red > 80%
      if (pct > 80) bar.style.background = '#f7768e';
      else if (pct > 50) bar.style.background = '#e0af68';
      else bar.style.background = '#9ece6a';
    }
  }

  tick();
}

function _stopLevelMeter() {
  if (_state.levelAnimFrame) {
    cancelAnimationFrame(_state.levelAnimFrame);
    _state.levelAnimFrame = null;
  }
}

// ── Live Speech for meeting recording ──

function _startLiveSpeech() {
  if (!_hasSpeechRecognition()) return;
  var SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  var recognition = new SpeechRecognition();
  recognition.continuous = true;
  recognition.interimResults = true;
  recognition.lang = _getSavedLang();

  _state.speechRecognition = recognition;

  recognition.onresult = function(event) {
    var text = '';
    for (var i = event.resultIndex; i < event.results.length; i++) {
      text += event.results[i][0].transcript;
    }
    // Send interim text to server
    if (_state.currentRecordingId && text) {
      var isFinal = event.results[event.results.length - 1].isFinal;
      _apiPost('/listening/interim', {
        recordingId: _state.currentRecordingId,
        text: text,
        isFinal: isFinal,
        timestamp: Date.now() - _state.startTime
      }).catch(function() {});
    }
  };

  recognition.onerror = function(event) {
    if (event.error === 'no-speech') return;
    console.error('[LI] Live speech error:', event.error);
  };

  recognition.onend = function() {
    if (_state.isRecording && (_state.currentMode === 'meeting' || _state.currentMode === 'email')) {
      try { recognition.start(); } catch(e) {}
    }
  };

  recognition.start();
}

function _stopLiveSpeech() {
  if (_state.speechRecognition) {
    _state.speechRecognition.onend = null;
    try { _state.speechRecognition.stop(); } catch(e) {}
    _state.speechRecognition = null;
  }
}

// ── Timer ──

function _startTimer() {
  _stopTimer();
  _state.timerInterval = setInterval(function() {
    _updateTimerDisplay();
  }, 1000);
}

function _stopTimer() {
  if (_state.timerInterval) {
    clearInterval(_state.timerInterval);
    _state.timerInterval = null;
  }
}

function _updateTimerDisplay() {
  var el = document.getElementById('li-timer');
  if (!el || !_state.startTime) return;
  var elapsed = Math.floor((Date.now() - _state.startTime) / 1000);
  var min = Math.floor(elapsed / 60);
  var sec = elapsed % 60;
  el.textContent = min + ':' + String(sec).padStart(2, '0');
}

// ── Waveform Visualization ──

LI.initWaveform = function(canvas) {
  if (!canvas || !_state.analyserNode) return;
  _state.waveformCanvas = canvas;
  _drawWaveform();
};

function _drawWaveform() {
  if (!_state.waveformCanvas || !_state.analyserNode || !_state.isRecording) return;

  var canvas = _state.waveformCanvas;
  var ctx = canvas.getContext('2d');
  var analyser = _state.analyserNode;
  var bufferLength = analyser.frequencyBinCount;
  var dataArray = new Uint8Array(bufferLength);

  function draw() {
    if (!_state.isRecording) return;
    _state.waveformAnimFrame = requestAnimationFrame(draw);

    analyser.getByteFrequencyData(dataArray);

    ctx.fillStyle = '#1a1b26';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    var barWidth = (canvas.width / bufferLength) * 2.5;
    var x = 0;

    for (var i = 0; i < bufferLength; i++) {
      var barHeight = (dataArray[i] / 255) * canvas.height;

      // Color gradient: blue to green based on amplitude
      var hue = 220 - (dataArray[i] / 255) * 100;
      ctx.fillStyle = 'hsl(' + hue + ', 70%, 60%)';
      ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);

      x += barWidth + 1;
      if (x > canvas.width) break;
    }
  }

  draw();
}

// ── Global Mic Button UI ──

function _updateGlobalMicUI() {
  var btn = document.getElementById('global-mic-btn');
  var pill = document.getElementById('li-recording-pill');

  if (_state.isRecording) {
    if (btn) btn.classList.add('li-recording');
    // For dictation, the indicator is inline near the target input.
    // For meeting/email, show the recording pill in the topbar.
    if (_state.currentMode === 'meeting' || _state.currentMode === 'email') {
      if (!pill) {
        var modeLabel = { meeting: 'Meeting', email: 'Email' };
        pill = document.createElement('div');
        pill.id = 'li-recording-pill';
        pill.className = 'li-recording-pill';

        var dot = document.createElement('span');
        dot.className = 'li-rec-dot';
        pill.appendChild(dot);

        var timer = document.createElement('span');
        timer.id = 'li-timer';
        timer.className = 'li-rec-timer';
        timer.textContent = '0:00';
        pill.appendChild(timer);

        var modeSpan = document.createElement('span');
        modeSpan.className = 'li-rec-mode';
        modeSpan.textContent = modeLabel[_state.currentMode] || '';
        pill.appendChild(modeSpan);

        var stopBtn = document.createElement('button');
        stopBtn.className = 'li-rec-stop';
        stopBtn.title = 'Stop recording';
        stopBtn.textContent = '\u25A0';
        stopBtn.addEventListener('click', function() { LI.stopRecording(); });
        pill.appendChild(stopBtn);

        var topbarRight = document.querySelector('.topbar-right');
        if (topbarRight) topbarRight.insertBefore(pill, topbarRight.firstChild);
      }
    }
  } else {
    if (btn) btn.classList.remove('li-recording');
    if (pill) pill.remove();
  }
}

// ── Mode Picker ──

LI.showModePicker = function(anchorEl) {
  LI.hideModePicker();
  var picker = document.createElement('div');
  picker.id = 'li-mode-picker';
  picker.className = 'li-mode-picker';

  var modes = [
    { id: 'dictation', label: 'Dictation (Speech to Text)' },
    { id: 'meeting', label: 'Meeting Recording' },
    { id: 'email', label: 'Email Draft' }
  ];

  var title = document.createElement('div');
  title.className = 'li-picker-title';
  title.textContent = 'Recording Mode';
  picker.appendChild(title);

  modes.forEach(function(mode) {
    var item = document.createElement('div');
    item.className = 'li-picker-item' + (mode.id === _state.lastUsedMode ? ' li-picker-active' : '');
    item.addEventListener('click', function() { LI._selectMode(mode.id); });

    var label = document.createElement('span');
    label.className = 'li-picker-label';
    label.textContent = mode.label;
    item.appendChild(label);

    picker.appendChild(item);
  });

  // Language selection section
  var langSection = document.createElement('div');
  langSection.id = 'li-lang-section';
  var langDivider = document.createElement('div');
  langDivider.className = 'li-picker-divider';
  langSection.appendChild(langDivider);
  var langTitle = document.createElement('div');
  langTitle.className = 'li-picker-title';
  langTitle.textContent = 'Language';
  langSection.appendChild(langTitle);

  var langs = [
    { code: 'auto', label: 'Auto (browser default)' },
    { code: 'en-US', label: 'English (US)' },
    { code: 'en-GB', label: 'English (UK)' },
    { code: 'zh-CN', label: '\u4E2D\u6587 (\u7B80\u4F53)' },
    { code: 'zh-TW', label: '\u4E2D\u6587 (\u7E41\u9AD4)' },
    { code: 'ja-JP', label: '\u65E5\u672C\u8A9E' },
    { code: 'ko-KR', label: '\uD55C\uAD6D\uC5B4' },
    { code: 'es-ES', label: 'Espa\u00F1ol' },
    { code: 'fr-FR', label: 'Fran\u00E7ais' },
    { code: 'de-DE', label: 'Deutsch' },
    { code: 'pt-BR', label: 'Portugu\u00EAs' }
  ];

  var savedLang = null;
  try { savedLang = localStorage.getItem('li-preferred-lang'); } catch(e) {}
  if (!savedLang) savedLang = 'auto';

  langs.forEach(function(lang) {
    var item = document.createElement('div');
    item.className = 'li-picker-item' + (lang.code === savedLang ? ' li-picker-active' : '');
    var label = document.createElement('span');
    label.className = 'li-picker-label';
    label.textContent = lang.label;
    item.appendChild(label);
    item.addEventListener('click', function() {
      try { localStorage.setItem('li-preferred-lang', lang.code); } catch(e) {}
      _showToast('Language: ' + lang.label);
      LI.hideModePicker();
    });
    langSection.appendChild(item);
  });
  picker.appendChild(langSection);

  // Mic selection section
  var micSection = document.createElement('div');
  micSection.id = 'li-mic-section';
  micSection.innerHTML = '<div class="li-picker-divider"></div><div class="li-picker-title">Microphone</div><div class="li-picker-item li-picker-loading"><span class="li-picker-label">Loading devices\u2026</span></div>';
  picker.appendChild(micSection);
  _populateMicPicker(micSection);

  // Position relative to anchor
  if (anchorEl) {
    var rect = anchorEl.getBoundingClientRect();
    picker.style.position = 'fixed';
    picker.style.top = (rect.bottom + 4) + 'px';
    picker.style.right = (window.innerWidth - rect.right) + 'px';
  }

  document.body.appendChild(picker);
  _state.modePickerVisible = true;

  // Close on outside click
  setTimeout(function() {
    document.addEventListener('click', _closeModePickerHandler, true);
  }, 0);
};

async function _populateMicPicker(section) {
  try {
    var devices = await navigator.mediaDevices.enumerateDevices();
    var inputs = devices.filter(function(d) { return d.kind === 'audioinput'; });
    var hasLabels = inputs.some(function(d) { return d.label && d.label.length > 0; });

    // If no labels, request permission first
    if (!hasLabels && inputs.length > 0) {
      try {
        var tempStream = await navigator.mediaDevices.getUserMedia({ audio: true });
        tempStream.getTracks().forEach(function(t) { t.stop(); });
        devices = await navigator.mediaDevices.enumerateDevices();
        inputs = devices.filter(function(d) { return d.kind === 'audioinput'; });
      } catch(e) { /* permission denied */ }
    }

    // Guard: picker may have been closed while we awaited
    if (!document.contains(section)) return;

    // Determine which mic is currently selected
    var savedId = null;
    try { savedId = localStorage.getItem('li-preferred-mic'); } catch(e) {}

    // Remove loading placeholder
    var loading = section.querySelector('.li-picker-loading');
    if (loading) loading.remove();

    if (inputs.length === 0) {
      var noMic = document.createElement('div');
      noMic.className = 'li-picker-item';
      noMic.innerHTML = '<span class="li-picker-label" style="color:#565f89">No microphones found</span>';
      section.appendChild(noMic);
      return;
    }

    // "Auto" option — clears saved preference, lets _selectMic auto-pick
    var autoItem = document.createElement('div');
    autoItem.className = 'li-picker-item' + (!savedId ? ' li-picker-active' : '');
    autoItem.innerHTML = '<span class="li-picker-label">Auto (prefer built-in)</span>';
    autoItem.addEventListener('click', function() {
      try { localStorage.removeItem('li-preferred-mic'); } catch(e) {}
      _state.micDeviceName = '';
      _showToast('Mic: Auto');
      LI.hideModePicker();
    });
    section.appendChild(autoItem);

    // Filter out duplicate deviceId entries
    var seen = {};
    var filtered = inputs.filter(function(d) {
      if (seen[d.deviceId]) return false;
      seen[d.deviceId] = true;
      return true;
    });

    filtered.forEach(function(d) {
      var item = document.createElement('div');
      var isActive = savedId && d.deviceId === savedId;
      item.className = 'li-picker-item' + (isActive ? ' li-picker-active' : '');

      var shortLabel = (d.label || 'Unknown')
        .replace(/\s*\(Built-in\)\s*$/i, '')
        .replace(/\s*Microphone\s*$/i, ' Mic')
        .replace(/Default - /, '');

      // Mark virtual/iPhone
      var badge = '';
      var lbl = d.label || '';
      if (lbl.includes('iPhone')) badge = ' <span class="li-mic-badge li-mic-phone">Phone</span>';
      else if (lbl.includes('Virtual') || lbl.includes('ZoomAudio') || lbl.includes('Teams Audio')) badge = ' <span class="li-mic-badge li-mic-virtual">Virtual</span>';
      else if (lbl.includes('Built-in') || lbl.includes('MacBook')) badge = ' <span class="li-mic-badge li-mic-builtin">Built-in</span>';

      item.innerHTML = '<span class="li-picker-label">' + _escText(shortLabel) + badge + '</span>';

      item.addEventListener('click', function() {
        try { localStorage.setItem('li-preferred-mic', d.deviceId); } catch(e) {}
        _state.micDeviceName = d.label;
        console.log('[LI] Mic selected:', d.label);
        _showToast('Mic: ' + shortLabel);
        LI.hideModePicker();
      });

      section.appendChild(item);
    });
  } catch(err) {
    console.warn('[LI] Failed to populate mic picker:', err);
    var errItem = section.querySelector('.li-picker-loading');
    if (errItem) errItem.innerHTML = '<span class="li-picker-label" style="color:#f7768e">Error listing devices</span>';
  }
}

function _closeModePickerHandler(e) {
  var picker = document.getElementById('li-mode-picker');
  if (picker && !picker.contains(e.target)) {
    LI.hideModePicker();
  }
}

LI.hideModePicker = function() {
  var picker = document.getElementById('li-mode-picker');
  if (picker) picker.remove();
  _state.modePickerVisible = false;
  document.removeEventListener('click', _closeModePickerHandler, true);
};

LI._selectMode = function(mode) {
  LI.hideModePicker();
  _state.lastUsedMode = mode;
  try { localStorage.setItem('li-last-mode', mode); } catch(e) {}

  var opts = {};
  // Auto-detect target for dictation
  if (mode === 'dictation') {
    opts.target = _findDictationTarget();
  }

  LI.startRecording(mode, opts);
};

// ── Global Mic Button Handlers ──
// Short click = toggle dictation to focused input
// Long press / right-click = show mode picker

LI.onMicClick = function() {
  if (_state.isRecording) {
    LI.stopRecording();
    return;
  }

  // Default: dictation to the focused/visible input
  var target = _findDictationTarget();
  if (!target) {
    // No input found — show a brief toast
    _showToast('Focus an input field first');
    return;
  }

  LI.startRecording('dictation', { target: target }).catch(function(err) {
    _showToast(err.message);
  });
};

LI.onMicDown = function(e) {
  // Prevent text selection and focus steal
  e.preventDefault();
  // Capture the input that had focus BEFORE clicking the mic button
  var active = document.activeElement;
  if (active && !_isExcludedTarget(active)) {
    if ((active.tagName === 'TEXTAREA' || (active.tagName === 'INPUT' && active.type === 'text')) ||
        active.isContentEditable) {
      _state._preMicTarget = active;
    }
  }
  // Capture button ref before event is cleared
  var btn = e.currentTarget;
  // Start long-press timer (500ms)
  _state.longPressTimer = setTimeout(function() {
    _state.longPressTimer = null;
    LI.showModePicker(btn);
  }, 500);
};

LI.onMicUp = function() {
  if (_state.longPressTimer) {
    clearTimeout(_state.longPressTimer);
    _state.longPressTimer = null;
    // Short click — toggle recording
    LI.onMicClick();
  }
  // If long-press already fired, do nothing (mode picker is shown)
};

LI.onMicContextMenu = function(e) {
  e.preventDefault();
  if (_state.longPressTimer) {
    clearTimeout(_state.longPressTimer);
    _state.longPressTimer = null;
  }
  LI.showModePicker(e.currentTarget);
};

// ── Toast (brief notification) ──

function _showToast(msg, duration) {
  var existing = document.getElementById('li-toast');
  if (existing) existing.remove();

  var toast = document.createElement('div');
  toast.id = 'li-toast';
  toast.className = 'li-toast';
  toast.textContent = msg;
  document.body.appendChild(toast);

  setTimeout(function() { toast.remove(); }, duration || 2500);
}

// ── Inline dictation for chat/prompt (backwards compat) ──

LI.startInlineDictation = function(targetElement, onText) {
  return LI.startRecording('dictation', { target: targetElement, onText: onText });
};

LI.stopInlineDictation = function() {
  return LI.stopRecording();
};

// ── State accessors ──
LI.isRecording = function() { return _state.isRecording; };
LI.getState = function() { return Object.assign({}, _state); };

// ── Cleanup helper ──
function _cleanup() {
  // Clean up terminal dictation textarea if it exists
  var termDictation = document.getElementById('li-terminal-dictation');
  if (termDictation) termDictation.remove();
  _state._isTerminalDictation = false;

  // Clean up dictation indicator
  var indicator = document.getElementById('li-dictation-indicator');
  if (indicator) indicator.remove();

  if (_state.mediaStream) {
    _state.mediaStream.getTracks().forEach(function(t) { t.stop(); });
    _state.mediaStream = null;
  }
  if (_state.audioContext) {
    _state.audioContext.close().catch(function() {});
    _state.audioContext = null;
  }
  _state.mediaRecorder = null;
  _state.analyserNode = null;
  _state.isRecording = false;
  _state.isPaused = false;
  _state.currentRecordingId = null;
  _stopTimer();
  _updateGlobalMicUI();
}

// ── Init ──
LI.init = function() {
  // Restore last-used mode, default to dictation
  try {
    var stored = localStorage.getItem('li-last-mode');
    // Migrate old modes to new simpler ones
    if (stored === 'dictation-chat' || stored === 'dictation-prompt') stored = 'dictation';
    _state.lastUsedMode = stored || 'dictation';
  } catch(e) {}
};

// Auto-init
LI.init();

window.LI = LI;
})();
