function createVoiceConversationCapture({
  Recognition, elements, transcriptStore = null, getLogin = () => '',
  createEvent = name => new Event(name, {bubbles:true}),
  maxLength = 0, draftLabel = '',
}) {
  const supported = typeof Recognition === 'function';
  const label = String(draftLabel || elements.root.dataset?.draftLabel || 'reply').trim() || 'reply';
  let recognition = null;
  let target = '';
  let generation = 0;
  elements.root.hidden = !supported;
  if (!supported) {
    elements.status.textContent = 'Voice capture is unavailable; type the ' + label + ' instead.';
  }

  const normalize = value => String(value || '').replace(/\s+/g, ' ').trim();
  const login = () => String(getLogin() || '').trim();
  const composerLimit = Number(elements.draft.maxLength);
  const limit = Number.isInteger(maxLength) && maxLength > 0 ? maxLength :
    Number.isInteger(composerLimit) && composerLimit > 0 ? composerLimit : 10000;

  function showReview(value, recovered = false) {
    const transcript = normalize(value).slice(0, limit);
    elements.transcript.value = transcript;
    elements.review.hidden = false;
    elements.append.textContent = elements.draft.value.trim() ? 'Append to draft' : 'Use transcript';
    elements.replace.hidden = !elements.draft.value.trim();
    elements.status.textContent = recovered ?
      'Recovered transcript. Review it before using or discarding it.' :
      'Transcript ready. Review it before using it.';
    if (!recovered && transcriptStore && login() && target) {
      void transcriptStore.save(login(), transcript, target);
    }
  }

  async function open(nextTarget) {
    cancel();
    target = String(nextTarget || '').trim();
    const opening = ++generation;
    elements.transcript.value = '';
    elements.review.hidden = true;
    elements.status.textContent = '';
    if (!supported || !target || !transcriptStore || !login()) return;
    const recovered = normalize(await transcriptStore.load(login(), target)).slice(0, limit);
    if (opening === generation && recovered) showReview(recovered, true);
  }

  function clearCheckpoint() {
    if (transcriptStore && login() && target) void transcriptStore.clear(login(), target);
  }

  function commit(replace) {
    const transcript = normalize(elements.transcript.value).slice(0, limit);
    elements.draft.value = replace ? transcript :
      [elements.draft.value.trim(), transcript].filter(Boolean).join('\n\n').slice(0, limit);
    elements.draft.dispatchEvent(createEvent('input'));
    clearCheckpoint();
    elements.review.hidden = true;
    elements.status.textContent = 'Transcript added. Review the ' + label + ' before sending.';
  }

  elements.start.addEventListener('click', () => {
    if (!supported || !target) return;
    const sessionGeneration = generation;
    const currentRecognition = new Recognition();
    recognition = currentRecognition;
    recognition.continuous = true;
    recognition.interimResults = false;
    recognition.onresult = event => {
      if (recognition !== currentRecognition || generation !== sessionGeneration) return;
      const finalText = Array.from(event.results || []).slice(event.resultIndex ?? 0)
        .filter(result => result.isFinal)
        .map(result => result[0]?.transcript || '').join(' ');
      if (normalize(finalText)) {
        showReview([elements.transcript.value, finalText].filter(Boolean).join(' '));
      }
    };
    recognition.onerror = event => {
      if (recognition !== currentRecognition || generation !== sessionGeneration) return;
      elements.start.hidden = false;
      elements.stop.hidden = true;
      elements.status.textContent = ['not-allowed', 'service-not-allowed'].includes(event.error) ?
        'Microphone permission was denied. Your draft and transcript are unchanged.' :
        'Voice capture stopped. Your draft and transcript are unchanged.';
    };
    recognition.onend = () => {
      if (recognition !== currentRecognition || generation !== sessionGeneration) return;
      recognition = null;
      elements.start.hidden = false;
      elements.stop.hidden = true;
    };
    try {
      recognition.start();
    } catch (_) {
      recognition = null;
      elements.start.hidden = false;
      elements.stop.hidden = true;
      elements.status.textContent = 'Voice capture could not start. Your draft and transcript are unchanged.';
      return;
    }
    elements.start.hidden = true;
    elements.stop.hidden = false;
    elements.status.textContent = 'Listening… Tap stop when you are finished.';
  });
  elements.stop.addEventListener('click', () => {
    recognition?.stop();
    elements.status.textContent = 'Finishing transcript…';
  });
  elements.append.addEventListener('click', () => commit(false));
  elements.replace.addEventListener('click', () => commit(true));
  elements.discard.addEventListener('click', () => {
    clearCheckpoint();
    elements.transcript.value = '';
    elements.review.hidden = true;
    elements.status.textContent = 'Transcript discarded.';
  });

  function cancel() {
    generation += 1;
    recognition?.abort();
    recognition = null;
    elements.start.hidden = false;
    elements.stop.hidden = true;
  }

  return {supported, open, cancel};
}

function mountVoiceConversation({
  kind, draftSelector, qs, Recognition, transcriptStore, getLogin,
  createCapture = createVoiceConversationCapture,
}) {
  return createCapture({
    Recognition, transcriptStore, getLogin,
    elements:{
      root:qs('#voice-' + kind), start:qs('#start-voice-' + kind),
      stop:qs('#stop-voice-' + kind), review:qs('#voice-' + kind + '-review'),
      transcript:qs('#voice-' + kind + '-transcript'), append:qs('#append-voice-' + kind),
      replace:qs('#replace-with-voice-' + kind), discard:qs('#discard-voice-' + kind),
      status:qs('#voice-' + kind + '-status'), draft:qs(draftSelector),
    },
  });
}

if (typeof module !== 'undefined') module.exports = {createVoiceConversationCapture, mountVoiceConversation};
