// ── WAV merge utility (chunked TTS + playlist export) ──────────────────────

async function mergeWavBlobs(blobs) {
  if (!blobs || blobs.length === 0) return null;
  if (blobs.length === 1) return blobs[0];

  function parseWav(bytes) {
    const v = new DataView(bytes.buffer);
    let off = 12, fmt = null, dataOff = 0, dataSize = 0;
    while (off + 8 <= bytes.length) {
      const id = v.getUint32(off, false);
      const sz = v.getUint32(off + 4, true);
      if (id === 0x666d7420) {
        fmt = { channels: v.getUint16(off+10,true), sampleRate: v.getUint32(off+12,true), bitDepth: v.getUint16(off+22,true) };
      } else if (id === 0x64617461) {
        dataOff = off + 8; dataSize = sz;
      }
      off += 8 + sz;
    }
    return { fmt, dataOff, dataSize };
  }

  const parsed = [];
  for (const b of blobs) {
    const bytes = new Uint8Array(await b.arrayBuffer());
    const p = parseWav(bytes);
    if (!p.fmt) throw new Error('Invalid WAV in chunk');
    parsed.push({ bytes, ...p });
  }
  const ref = parsed[0].fmt;
  const totalPcm = parsed.reduce((s, p) => s + p.dataSize, 0);
  const out = new Uint8Array(44 + totalPcm);
  const dv = new DataView(out.buffer);
  dv.setUint32(0,  0x52494646, false);
  dv.setUint32(4,  36 + totalPcm, true);
  dv.setUint32(8,  0x57415645, false);
  dv.setUint32(12, 0x666d7420, false);
  dv.setUint32(16, 16, true);
  dv.setUint16(20, 1, true);
  dv.setUint16(22, ref.channels, true);
  dv.setUint32(24, ref.sampleRate, true);
  dv.setUint32(28, ref.sampleRate * ref.channels * (ref.bitDepth >> 3), true);
  dv.setUint16(32, ref.channels * (ref.bitDepth >> 3), true);
  dv.setUint16(34, ref.bitDepth, true);
  dv.setUint32(36, 0x64617461, false);
  dv.setUint32(40, totalPcm, true);
  let pos = 44;
  for (const p of parsed) {
    out.set(p.bytes.slice(p.dataOff, p.dataOff + p.dataSize), pos);
    pos += p.dataSize;
  }
  return new Blob([out], { type: 'audio/wav' });
}

// ── Chunked TTS ────────────────────────────────────────────────────────────

function splitTextIntoChunks(text, maxLen = 800) {
  const abbrev = /\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|vs|etc|e\.g|i\.e)\.\s/g;
  const safe = text.replace(abbrev, m => m.replace('.', '\x00'));
  const restore = s => s.replace(/\x00/g, '.');

  // Gather segments from each line: sentences first, then any remaining line text.
  // This ensures newline-delimited text (e.g. German bullet lists) gets split too.
  const segments = [];
  for (const line of safe.split('\n')) {
    const sentences = line.match(/[^.!?]+[.!?]+\s*/g) || [];
    const rest = line.replace(/[^.!?]+[.!?]+\s*/g, '').trim();
    segments.push(...sentences);
    if (rest) segments.push(rest);
  }
  if (!segments.length) return [text];

  const chunks = [];
  let cur = '';
  for (const seg of segments) {
    const joined = cur ? cur + ' ' + seg.trim() : seg.trim();
    if (joined.length > maxLen && cur) { chunks.push(restore(cur.trim())); cur = seg.trim(); }
    else cur = joined;
  }
  if (cur.trim()) chunks.push(restore(cur.trim()));
  return chunks.length ? chunks : [text];
}

async function generateChunkedTts(voice, text, backend, instruct, extra = null) {
  const chunks = splitTextIntoChunks(text);
  const prog = $('preview-chunk-progress');
  if (prog) { prog.hidden = false; prog.textContent = `Chunk 1 / ${chunks.length}…`; }
  const blobs = [];
  for (let i = 0; i < chunks.length; i++) {
    if (prog) prog.textContent = `Chunk ${i + 1} / ${chunks.length}…`;
    blobs.push(await fetchTtsPreviewBlob(voice, chunks[i], 'wav', instruct, backend, false, extra));
  }
  if (prog) prog.textContent = 'Merging…';
  const merged = await mergeWavBlobs(blobs);
  if (prog) { prog.hidden = true; prog.textContent = ''; }
  return { url: URL.createObjectURL(merged), blob: merged, streaming: false, label: 'chunked' };
}

// ── Generation history ─────────────────────────────────────────────────────

const _genHistory = [];

function historyPush(voice, text, backend, blob, url) {
  const id = Date.now() + '-' + Math.random().toString(36).slice(2, 6);
  _genHistory.unshift({ id, ts: Date.now(), voice, text: text.slice(0, 200), backend, blob, url });
  if (_genHistory.length > 20) _genHistory.pop();
  renderHistory();
}

function _histAvatarColor(id) {
  const palette = ['#3b82f6','#10b981','#8b5cf6','#f59e0b','#ef4444','#ec4899','#06b6d4','#84cc16'];
  let h = 0; for (let i = 0; i < (id||'').length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
  return palette[h % palette.length];
}

function renderHistory() {
  const list = $('history-list');
  if (!list) return;
  if (!_genHistory.length) {
    list.innerHTML = '<div class="history-empty">No generations yet.</div>';
    return;
  }
  list.innerHTML = _genHistory.map(item => {
    const t = new Date(item.ts);
    const ts = String(t.getHours()).padStart(2,'0') + ':' + String(t.getMinutes()).padStart(2,'0');
    const preview = escHtml(item.text.length > 90 ? item.text.slice(0,90) + '…' : item.text);
    const v = (window._voices || []).find(vx => vx.id === item.voice);
    const avatar = v?.has_picture
      ? `<img src="/api/voice/picture/${encodeURIComponent(item.voice)}" class="hist-avatar" alt="">`
      : `<span class="hist-avatar hist-avatar-init" style="background:${_histAvatarColor(item.voice)}">${(item.voice||'?')[0].toUpperCase()}</span>`;
    return `<div class="history-item" data-hid="${escHtml(item.id)}">
      <div class="history-item-row1">
        ${avatar}
        <span class="history-voice">${escHtml(item.voice)}</span>
        <span class="history-backend">${escHtml(item.backend)}</span>
        <span class="history-time">${ts}</span>
        <button class="hist-del-btn" title="Delete"><span class="mdi mdi-close"></span></button>
      </div>
      <div class="history-item-text">${preview}</div>
      <div class="history-item-actions">
        <button class="btn-secondary btn-xs hist-play-btn" ${item.blob?'':'disabled'}><span class="mdi mdi-play"></span> Play</button>
        <button class="btn-secondary btn-xs hist-reuse-btn"><span class="mdi mdi-restore"></span> Reuse</button>
        <button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?'':'disabled'}><span class="mdi mdi-plus"></span> Playlist</button>
      </div>
    </div>`;
  }).join('');

  list.querySelectorAll('.hist-del-btn').forEach(btn => {
    btn.addEventListener('click', e => {
      e.stopPropagation();
      const hid = btn.closest('[data-hid]')?.dataset.hid;
      const idx = _genHistory.findIndex(h => h.id === hid);
      if (idx !== -1) { _genHistory.splice(idx, 1); renderHistory(); }
    });
  });
  list.querySelectorAll('.hist-play-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const item = _genHistory.find(h => h.id === btn.closest('[data-hid]')?.dataset.hid);
      if (!item?.url) return;
      const audio = $('preview-audio');
      audio.src = item.url; audio.style.display = ''; audio.play().catch(()=>{});
    });
  });
  list.querySelectorAll('.hist-reuse-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const item = _genHistory.find(h => h.id === btn.closest('[data-hid]')?.dataset.hid);
      if (!item) return;
      $('preview-text-area').value = item.text;
      const bSel = $('tts-backend-select');
      if (bSel) [...bSel.options].forEach(o => { if (o.value === item.backend) bSel.value = item.backend; });
      const vSel = $('tts-voice-select');
      if (vSel) [...vSel.options].forEach(o => { if (o.value === item.voice) vSel.value = item.voice; });
      toast('Settings restored from history', 'success');
    });
  });
  list.querySelectorAll('.hist-playlist-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const item = _genHistory.find(h => h.id === btn.closest('[data-hid]')?.dataset.hid);
      if (item?.blob) playlistAdd(item.voice, item.text, item.blob, item.url);
    });
  });
}

$('history-clear-btn')?.addEventListener('click', () => {
  _genHistory.length = 0; renderHistory();
  toast('History cleared', 'success');
});

// ── Playlist ───────────────────────────────────────────────────────────────

const _playlist = [];

function playlistAdd(voice, text, blob, url) {
  const id = 'pl-' + Date.now() + '-' + Math.random().toString(36).slice(2,5);
  _playlist.push({ id, voice, text: text.slice(0, 120), blob, url });
  renderPlaylist();
  if ($('playlist-export-btn')) $('playlist-export-btn').disabled = false;
  toast('Added to playlist', 'success');
}

function renderPlaylist() {
  const list = $('playlist-list');
  if (!list) return;
  if (!_playlist.length) {
    list.innerHTML = '<div class="history-empty">No clips in playlist. Use <strong>+ Playlist</strong> after generating.</div>';
    if ($('playlist-export-btn')) $('playlist-export-btn').disabled = true;
    return;
  }
  list.innerHTML = _playlist.map((item, i) => `
    <div class="playlist-item" data-pid="${escHtml(item.id)}">
      <span class="playlist-num">${i + 1}</span>
      <div class="playlist-info">
        <span class="history-voice">${escHtml(item.voice)}</span>
        <span class="playlist-text">${escHtml(item.text.length > 70 ? item.text.slice(0,70)+'…' : item.text)}</span>
      </div>
      <div class="playlist-item-actions">
        <button class="btn-secondary btn-xs pl-up" ${i===0?'disabled':''}><span class="mdi mdi-upload"></span></button>
        <button class="btn-secondary btn-xs pl-dn" ${i===_playlist.length-1?'disabled':''}><span class="mdi mdi-download"></span></button>
        <button class="btn-secondary btn-xs pl-rm"><span class="mdi mdi-close"></span></button>
      </div>
    </div>`).join('');

  list.querySelectorAll('.pl-rm').forEach(btn => {
    btn.addEventListener('click', () => {
      const pid = btn.closest('[data-pid]')?.dataset.pid;
      const idx = _playlist.findIndex(p => p.id === pid);
      if (idx >= 0) { _playlist.splice(idx, 1); renderPlaylist(); }
    });
  });
  list.querySelectorAll('.pl-up').forEach(btn => {
    btn.addEventListener('click', () => {
      const pid = btn.closest('[data-pid]')?.dataset.pid;
      const idx = _playlist.findIndex(p => p.id === pid);
      if (idx > 0) { [_playlist[idx-1], _playlist[idx]] = [_playlist[idx], _playlist[idx-1]]; renderPlaylist(); }
    });
  });
  list.querySelectorAll('.pl-dn').forEach(btn => {
    btn.addEventListener('click', () => {
      const pid = btn.closest('[data-pid]')?.dataset.pid;
      const idx = _playlist.findIndex(p => p.id === pid);
      if (idx < _playlist.length - 1) { [_playlist[idx], _playlist[idx+1]] = [_playlist[idx+1], _playlist[idx]]; renderPlaylist(); }
    });
  });
}

$('add-to-playlist-btn')?.addEventListener('click', () => {
  if (!previewBlob) { toast('Generate audio first', 'error'); return; }
  playlistAdd(
    window._previewVoice   || $('tts-voice-select')?.value  || '',
    window._previewText    || $('preview-text-area')?.value || '',
    previewBlob,
    $('preview-audio')?.src || ''
  );
});

$('playlist-export-btn')?.addEventListener('click', async () => {
  if (!_playlist.length) return;
  const btn = $('playlist-export-btn'), orig = btn.textContent;
  btn.disabled = true; btn.textContent = 'Merging…';
  try {
    const merged = await mergeWavBlobs(_playlist.map(p => p.blob).filter(Boolean));
    if (!merged) throw new Error('No audio to export');
    const a = document.createElement('a');
    a.href = URL.createObjectURL(merged);
    a.download = 'playlist_' + Date.now() + '.wav'; a.click();
    toast('Playlist exported as WAV', 'success');
  } catch(e) { toast('Export failed: ' + e.message, 'error'); }
  finally { btn.disabled = _playlist.length === 0; btn.textContent = orig; }
});

$('playlist-clear-btn')?.addEventListener('click', () => {
  _playlist.length = 0; renderPlaylist();
  toast('Playlist cleared', 'success');
});

// ── Audio effects panel ────────────────────────────────────────────────────

const _FX_PRESETS = {
  studio:    { reverb:      { on:true,  room_size:0.6,  wet:0.35 },
               compressor:  { on:true,  threshold_db:-18, ratio:3 } },
  broadcast: { compressor:  { on:true,  threshold_db:-12, ratio:6 } },
  telephone: { compressor:  { on:true,  threshold_db:-10, ratio:8 } },
  warm:      { reverb:      { on:true,  room_size:0.2,  wet:0.15 },
               compressor:  { on:true,  threshold_db:-20, ratio:2 } },
  radio:     { compressor:  { on:true,  threshold_db:-14, ratio:5 } },
};

function fxSliderBind(sliderId, labelId, fmt) {
  const s = $(sliderId), l = $(labelId);
  if (!s || !l) return;
  const upd = () => { l.textContent = fmt(s.value); };
  upd(); s.addEventListener('input', upd);
}
fxSliderBind('fx-reverb-room', 'fx-reverb-room-val', v => parseFloat(v).toFixed(2));
fxSliderBind('fx-reverb-wet',  'fx-reverb-wet-val',  v => parseFloat(v).toFixed(2));
fxSliderBind('fx-comp-thresh', 'fx-comp-thresh-val', v => v + ' dB');
fxSliderBind('fx-comp-ratio',  'fx-comp-ratio-val',  v => v + ':1');
fxSliderBind('fx-chorus-rate', 'fx-chorus-rate-val', v => parseFloat(v).toFixed(1) + ' Hz');
fxSliderBind('fx-chorus-mix',  'fx-chorus-mix-val',  v => parseFloat(v).toFixed(2));
fxSliderBind('fx-pitch-semi',  'fx-pitch-semi-val',  v => (parseFloat(v) >= 0 ? '+' : '') + v + ' st');

$('effects-preset')?.addEventListener('change', () => {
  const preset = _FX_PRESETS[$('effects-preset').value];
  if (!preset) return;
  ['fx-reverb-on','fx-compressor-on','fx-chorus-on','fx-pitch-on'].forEach(id => { const el=$(id); if(el) el.checked=false; });
  if (preset.reverb) {
    $('fx-reverb-on').checked = !!preset.reverb.on;
    if (preset.reverb.room_size != null) $('fx-reverb-room').value = preset.reverb.room_size;
    if (preset.reverb.wet       != null) $('fx-reverb-wet').value  = preset.reverb.wet;
  }
  if (preset.compressor) {
    $('fx-compressor-on').checked = !!preset.compressor.on;
    if (preset.compressor.threshold_db != null) $('fx-comp-thresh').value = preset.compressor.threshold_db;
    if (preset.compressor.ratio        != null) $('fx-comp-ratio').value  = preset.compressor.ratio;
  }
  ['fx-reverb-room','fx-reverb-wet','fx-comp-thresh','fx-comp-ratio','fx-chorus-rate','fx-chorus-mix','fx-pitch-semi']
    .forEach(id => $(id)?.dispatchEvent(new Event('input')));
});

$('effects-reset-btn')?.addEventListener('click', () => {
  $('effects-preset').value = '';
  ['fx-reverb-on','fx-compressor-on','fx-chorus-on','fx-pitch-on'].forEach(id => { const el=$(id); if(el) el.checked=false; });
  $('fx-reverb-room').value = '0.35'; $('fx-reverb-wet').value = '0.25';
  $('fx-comp-thresh').value = '-20';  $('fx-comp-ratio').value = '4';
  $('fx-chorus-rate').value = '1';    $('fx-chorus-mix').value = '0.5';
  $('fx-pitch-semi').value  = '0';
  ['fx-reverb-room','fx-reverb-wet','fx-comp-thresh','fx-comp-ratio','fx-chorus-rate','fx-chorus-mix','fx-pitch-semi']
    .forEach(id => $(id)?.dispatchEvent(new Event('input')));
});

let _effectsSourceBlob = null;

$('effects-apply-btn')?.addEventListener('click', async () => {
  const blob = previewBlob || _effectsSourceBlob;
  if (!blob) { toast('Generate audio first', 'error'); return; }
  const chain = [];
  if ($('fx-reverb-on')?.checked)    chain.push({ type:'reverb',      params: { room_size: +$('fx-reverb-room').value, wet: +$('fx-reverb-wet').value, dry: 1 - +$('fx-reverb-wet').value } });
  if ($('fx-compressor-on')?.checked) chain.push({ type:'compressor',  params: { threshold_db: +$('fx-comp-thresh').value, ratio: +$('fx-comp-ratio').value } });
  if ($('fx-chorus-on')?.checked)    chain.push({ type:'chorus',       params: { rate_hz: +$('fx-chorus-rate').value, mix: +$('fx-chorus-mix').value } });
  if ($('fx-pitch-on')?.checked)     chain.push({ type:'pitch_shift',  params: { semitones: +$('fx-pitch-semi').value } });
  if (!chain.length) { toast('Enable at least one effect', 'error'); return; }
  const btn = $('effects-apply-btn'), st = $('effects-status');
  btn.disabled = true; if (st) st.textContent = 'Processing…';
  try {
    const fd = new FormData();
    fd.append('audio', blob, 'audio.wav');
    fd.append('effects', JSON.stringify(chain));
    const resp = await fetch('/api/audio/effects', { method:'POST', body: fd });
    if (!resp.ok) { const e = await resp.json().catch(()=>({})); throw new Error(e.detail || resp.statusText); }
    const out = await resp.blob();
    if (!_effectsSourceBlob) _effectsSourceBlob = previewBlob;
    previewBlob = out;
    const audio = $('preview-audio');
    audio.src = URL.createObjectURL(out); audio.style.display = ''; audio.play().catch(()=>{});
    if (st) st.textContent = 'Applied.';
    toast('Effects applied', 'success');
  } catch(e) { if (st) st.textContent = ''; toast('Effects failed: ' + e.message, 'error'); }
  finally { btn.disabled = false; }
});

// ── LLM refinement ─────────────────────────────────────────────────────────

let _refineOriginal = null;

(function initLlmRefinement() {
  const inp = $('refine-llm-url');
  if (!inp) return;
  // loadSettings() will overwrite with the server value; localStorage is the fast initial fallback
  const saved = (_appSettings && _appSettings.refine_llm_url) || localStorage.getItem('refine-llm-url');
  if (saved) inp.value = saved;
  else inp.value = (_appSettings && _appSettings.llm_url) || (_appSettings && _appSettings.engine_local_urls && _appSettings.engine_local_urls['ollama']) || localStorage.getItem('llm-local-url-ollama') || 'http://localhost:11434/v1';
  inp.addEventListener('input', () => {
    localStorage.setItem('refine-llm-url', inp.value);
    _patchSettings({ refine_llm_url: inp.value });
    if (_appSettings) _appSettings.refine_llm_url = inp.value;
  });
})();

function updateRefineButtonState() {
  const btn = $('refine-btn');
  if (btn) btn.disabled = !$('stt-tts-text')?.value?.trim();
}
$('stt-tts-text')?.addEventListener('input', updateRefineButtonState);

$('refine-btn')?.addEventListener('click', async () => {
  const text = $('stt-tts-text')?.value?.trim();
  if (!text) return;
  const btn = $('refine-btn'), st = $('refine-status');
  btn.disabled = true; if (st) st.textContent = 'Refining…';
  try {
    const r = await fetch('/api/refine-text', { method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({
        text,
        llm_url:  $('refine-llm-url')?.value?.trim()  || _appSettings?.llm_url || 'http://localhost:11434/v1',
        model:    $('refine-model')?.value?.trim()    || _appSettings?.refine_model || _appSettings?.llm_model || '',
        toggles: {
          fillers:     $('refine-fillers')?.checked     ?? true,
          repetitions: $('refine-repetitions')?.checked ?? true,
          corrections: $('refine-corrections')?.checked ?? true,
          punctuation: $('refine-punctuation')?.checked ?? true,
        },
      }),
    });
    if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
    const d = await r.json();
    _refineOriginal = text;
    $('stt-tts-text').value = d.text;
    if ($('refine-restore-btn')) $('refine-restore-btn').disabled = false;
    if (st) st.textContent = 'Done.';
    toast('Transcription refined', 'success');
  } catch(e) { if (st) st.textContent = ''; toast('Refinement failed: ' + e.message, 'error'); }
  finally { btn.disabled = !$('stt-tts-text')?.value?.trim(); }
});

$('refine-restore-btn')?.addEventListener('click', () => {
  if (!_refineOriginal) return;
  $('stt-tts-text').value = _refineOriginal; _refineOriginal = null;
  if ($('refine-restore-btn')) $('refine-restore-btn').disabled = true;
  if ($('refine-status')) $('refine-status').textContent = '';
  toast('Original transcription restored', 'success');
});

// Update style-support warning when user types in style field
$('preview-style-instruction')?.addEventListener('input', () => {
  if (typeof updateBackendHelp === 'function') updateBackendHelp();
});

