// ── TTS routing ──────────────────────────────────────────────────────────

let _ttsRoutes = [];
const ROUTE_LANGS = [
  ['*', 'Any'],
  ['AUTO', 'Auto'],
  ['EN', 'English'],
  ['DE', 'German'],
  ['FR', 'French'],
  ['ES', 'Spanish'],
  ['IT', 'Italian'],
  ['PT', 'Portuguese'],
  ['NL', 'Dutch'],
  ['PL', 'Polish'],
];

function routeSelectOptions(value) {
  return ROUTE_LANGS.map(([code, label]) =>
    `<option value="${code}" ${code === value ? 'selected' : ''}>${escHtml(label)}</option>`
  ).join('');
}

const ROUTE_BACKENDS = [
  ['voice_clone', 'Voice Clone'],
  ['streaming', 'Streaming'],
  ['voice_design', 'Voice Design'],
  ['nvidia_magpie', 'NVIDIA Magpie'],
  ['nvidia_zeroshot', 'NVIDIA Zeroshot'],
  ['nvidia_flow', 'NVIDIA Flow'],
];
let _routeSounds = [];

function routeSoundOptions(value = '') {
  const current = String(value || '');
  const listed = new Set(_routeSounds.map(s => String(s.path || '')));
  const options = ['<option value="">Browse sounds</option>'];
  if (current && !listed.has(current)) options.push(`<option value="${escHtml(current)}" selected>${escHtml(current)}</option>`);
  _routeSounds.forEach(sound => {
    const path = String(sound.path || '');
    if (!path) return;
    const duration = sound.duration != null ? ` · ${Number(sound.duration).toFixed(1)}s` : '';
    options.push(`<option value="${escHtml(path)}" ${path === current ? 'selected' : ''}>${escHtml(path + duration)}</option>`);
  });
  return options.join('');
}

async function loadRouteSounds() {
  try {
    const r = await fetch('/api/route-sounds');
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
    const d = await r.json();
    _routeSounds = Array.isArray(d.sounds) ? d.sounds : [];
  } catch (_) {
    _routeSounds = [];
  }
}

let _routeSoundPickerTarget = null;
let _routeSoundPlayingButton = null;

function routeSoundUrl(path) {
  return '/api/route-sounds/file/' + String(path || '').split('/').map(encodeURIComponent).join('/');
}

function setRouteSoundField(row, target, path) {
  const field = row?.querySelector(target === 'before' ? '.route-before-sound' : '.route-after-sound');
  if (!field) return;
  field.value = path || '';
  readRoutingForm();
}

function renderRouteSoundBrowser() {
  const list = $('routing-sound-list');
  if (!list) return;
  const query = String($('routing-sound-search')?.value || '').trim().toLowerCase();
  const sounds = query
    ? _routeSounds.filter(sound => String(sound.path || '').toLowerCase().includes(query) || String(sound.name || '').toLowerCase().includes(query))
    : _routeSounds;
  if (!_routeSounds.length) {
    list.innerHTML = '<div class="routing-log-empty">No sounds found yet. Upload one in a route row; only the selected file is imported.</div>';
    return;
  }
  if (!sounds.length) {
    list.innerHTML = '<div class="routing-log-empty">No sounds match this search.</div>';
    return;
  }
  list.innerHTML = sounds.map(sound => {
    const path = String(sound.path || '');
    const size = sound.size != null ? Math.max(1, Math.round(Number(sound.size) / 1024)) + ' KB' : '';
    const duration = sound.duration != null ? Number(sound.duration).toFixed(1) + 's' : size;
    const type = sound.type ? String(sound.type).toUpperCase() : '';
    const targetLabel = _routeSoundPickerTarget?.target === 'after' ? 'Use after' : 'Use before';
    return `
      <div class="routing-sound-item" data-path="${escHtml(path)}">
        <button class="btn-secondary sound-play" type="button" title="Preview sound">▶</button>
        <div class="sound-path" title="${escHtml(path)}">${escHtml(path)}</div>
        <div class="sound-meta">${escHtml(duration || '-')} ${type ? '· ' + escHtml(type) : ''}</div>
        <button class="btn-secondary sound-use-current" type="button">${escHtml(targetLabel)}</button>
      </div>`;
  }).join('');
}

async function openRouteSoundBrowser(row, target) {
  _routeSoundPickerTarget = {row, target};
  await loadRouteSounds();
  renderRouteSoundBrowser();
  const panel = $('routing-sound-browser');
  if (panel) {
    panel.hidden = false;
    panel.scrollIntoView({block:'nearest', behavior:'smooth'});
  }
  const label = target === 'before' ? 'before sound' : 'after sound';
  const note = $('routing-sound-browser-note');
  if (note) note.textContent = `Preview uploaded route sounds, then choose one for this ${label}. Upload imports only the selected file; this list also shows sounds already present in the sounds folders. ${_routeSounds.length} sounds available.`;
}

function closeRouteSoundBrowser() {
  const panel = $('routing-sound-browser');
  if (panel) panel.hidden = true;
  const audio = $('routing-sound-preview');
  if (audio) { audio.pause(); audio.hidden = true; audio.removeAttribute('src'); }
  if (_routeSoundPlayingButton) _routeSoundPlayingButton.textContent = '▶';
  _routeSoundPlayingButton = null;
  _routeSoundPickerTarget = null;
}

function playRouteSound(path, btn) {
  const audio = $('routing-sound-preview');
  if (!audio || !path) return;
  if (_routeSoundPlayingButton && _routeSoundPlayingButton !== btn) _routeSoundPlayingButton.textContent = '▶';
  _routeSoundPlayingButton = btn;
  btn.textContent = '❚❚';
  audio.hidden = false;
  audio.src = routeSoundUrl(path);
  audio.onended = () => { btn.textContent = '▶'; };
  audio.onpause = () => { if (_routeSoundPlayingButton === btn) btn.textContent = '▶'; };
  audio.onplay = () => { btn.textContent = '❚❚'; };
  audio.play().catch(e => {
    btn.textContent = '▶';
    toast('Sound preview failed: ' + e.message, 'error');
  });
}

function useRouteSound(path, target) {
  const selected = _routeSoundPickerTarget || {};
  const row = selected.row || document.querySelector('.routing-row');
  const useTarget = target || selected.target || 'before';
  setRouteSoundField(row, useTarget, path);
  toast(`${useTarget === 'before' ? 'Before' : 'After'} sound selected`, 'success');
}

function routeBackendOptions(value) {
  const current = value || 'voice_clone';
  return ROUTE_BACKENDS.map(([code, label]) =>
    `<option value="${code}" ${code === current ? 'selected' : ''}>${escHtml(label)}</option>`
  ).join('');
}

function refreshRoutingVoiceOptions() {
  const dl = $('routing-voice-options');
  if (dl) {
    const ids = [...activeVoiceIds(), ...virtualDesignVoiceIds()];
    dl.innerHTML = [...new Set(ids)].map(id => `<option value="${escHtml(id)}"></option>`).join('');
  }
  const soundsDl = $('routing-sound-options');
  if (soundsDl) soundsDl.innerHTML = _routeSounds.map(sound => `<option value="${escHtml(sound.path || '')}"></option>`).join('');
}

function newRoute(app = 'Open WebUI', inputVoice = 'default', language = '*', outputVoice = '') {
  return {
    id: 'route_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6),
    enabled: true,
    app,
    input_voice: inputVoice,
    language,
    backend: 'voice_clone',
    output_voice: outputVoice,
    before_sound: '',
    after_sound: '',
  };
}

function renderRoutingList() {
  if (!$('routing-list')) return;
  refreshRoutingVoiceOptions();
  $('routing-proxy-url').textContent = getCreatorV1Url();
  updateCreatorUrlHints();
  $('routing-status').textContent = _ttsRoutes.length ? `${_ttsRoutes.length} route${_ttsRoutes.length === 1 ? '' : 's'}` : 'No routes yet.';
  if (!_ttsRoutes.length) {
    $('routing-list').innerHTML = '<div class="note" style="padding:10px">No routing rules yet. Add a route or add the Open WebUI default examples.</div>';
    return;
  }
  $('routing-list').innerHTML = _ttsRoutes.map((r, i) => `
    <div class="routing-grid routing-row" data-index="${i}">
      <label class="toggle" title="Enable route">
        <input type="checkbox" class="route-enabled" aria-label="Enable route" ${r.enabled !== false ? 'checked' : ''}>
        <span class="t-slider"></span>
      </label>
      <input class="route-app" aria-label="App / client" value="${escHtml(r.app || 'Open WebUI')}" placeholder="Open WebUI, SillyTavern, Home Assistant, or *" title="Matches app/client JSON, X-TTS-App header, or detected client name. Use * for any app.">
      <input class="route-input" aria-label="Input voice" value="${escHtml(r.input_voice || 'default')}" placeholder="default">
      <select class="route-lang" aria-label="Route language">${routeSelectOptions(String(r.language || '*').toUpperCase())}</select>
      <select class="route-backend" aria-label="Route backend" title="Voice Clone uses the normal TTS URL, Streaming uses the streaming URL, Voice Design uses vd_ presets, NVIDIA Magpie uses fixed NVIDIA voices, NVIDIA Zeroshot/Flow use saved library WAVs as audio prompts.">${routeBackendOptions(r.backend || 'voice_clone')}</select>
      <input class="route-output" value="${escHtml(r.output_voice || '')}" list="routing-voice-options" placeholder="EN_F_VoiceName or vd_Preset">
      <div class="route-sound-cell">
        <input class="route-before-sound" value="${escHtml(r.before_sound || '')}" placeholder="sounds/start.wav" list="routing-sound-options">
        <button class="btn-secondary route-sound-pick" data-target="before" title="Browse and preview uploaded sounds">Pick</button>
        <button class="btn-secondary route-sound-upload" data-target="before" title="Upload one before sound">Upload</button>
      </div>
      <div class="route-sound-cell">
        <input class="route-after-sound" value="${escHtml(r.after_sound || '')}" placeholder="sounds/end.wav" list="routing-sound-options">
        <button class="btn-secondary route-sound-pick" data-target="after" title="Browse and preview uploaded sounds">Pick</button>
        <button class="btn-secondary route-sound-upload" data-target="after" title="Upload one after sound">Upload</button>
      </div>
      <button class="btn-secondary routing-delete" title="Delete route">×</button>
    </div>
  `).join('');
}

function readRoutingForm() {
  _ttsRoutes = [...document.querySelectorAll('.routing-row')].map((row, i) => {
    const existing = _ttsRoutes[Number(row.dataset.index)] || {};
    return {
      id: existing.id || `route_${i+1}`,
      enabled: row.querySelector('.route-enabled').checked,
      app: row.querySelector('.route-app').value.trim() || '*',
      input_voice: row.querySelector('.route-input').value.trim() || 'default',
      language: row.querySelector('.route-lang').value || '*',
      backend: row.querySelector('.route-backend').value || 'voice_clone',
      output_voice: row.querySelector('.route-output').value.trim(),
      before_sound: row.querySelector('.route-before-sound').value.trim(),
      after_sound: row.querySelector('.route-after-sound').value.trim(),
    };
  });
}

async function loadRoutingTab() {
  if (!$('routing-list')) return;
  $('routing-proxy-url').textContent = getCreatorV1Url();
  updateCreatorUrlHints();
  $('routing-status').textContent = 'Loading routing…';
  $('routing-list').innerHTML = loadingMarkup('Loading routing', 'Loading active voices and routing rules for the proxy.', 5);
  setBusyButton('routing-refresh-btn', true);
  // Load voices + sounds in parallel (non-fatal — routing still works without them)
  await Promise.allSettled([
    _voices.length ? Promise.resolve() : loadVoiceLibrary().catch(() => {}),
    loadRouteSounds(),
  ]);
  try {
    const r = await fetch('/api/tts-routes');
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
    const d = await r.json();
    _ttsRoutes = Array.isArray(d.routes) ? d.routes : [];
    renderRoutingList();
    status('Routing loaded');
    loadRoutingLog();
  } catch(e) {
    $('routing-status').textContent = 'Load failed';
    $('routing-list').innerHTML = `<div style="color:var(--red);padding:10px">Failed to load routes: ${escHtml(e.message)}</div>`;
    status('Routing load failed');
  } finally {
    setBusyButton('routing-refresh-btn', false);
  }
}

async function saveRoutingTab() {
  readRoutingForm();
  $('routing-save-btn').disabled = true;
  try {
    const r = await fetch('/api/tts-routes', {
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({routes:_ttsRoutes}),
    });
    if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
    const d = await r.json();
    _ttsRoutes = d.routes || _ttsRoutes;
    renderRoutingList();
    toast('Routing saved', 'success');
    status('TTS routing saved');
  } catch(e) {
    toast('Save routes failed: ' + e.message, 'error');
  } finally {
    $('routing-save-btn').disabled = false;
  }
}

function renderRouteTestResult(d) {
  const el = $('routing-test-result');
  if (!el) return;
  const health = d.voice_health || {};
  const warnings = Array.isArray(health.warnings) ? health.warnings : [];
  el.className = 'routing-test-result ' + (warnings.length ? 'warn' : 'ok');
  const parts = [
    `${escHtml(d.requested_voice || '')} → ${escHtml(d.routed_voice || '')}`,
    `app ${escHtml(d.app || '-')}`,
    `backend ${escHtml(d.backend || 'voice_clone')}`,
    `language ${escHtml(d.detected_language || '-')}`,
    d.matched ? 'matched route' : 'no route matched',
  ];
  if (health.duration) parts.push(`reference ${health.duration}s`);
  if (health.word_count != null) parts.push(`${health.word_count} words`);
  if (health.words_per_sec) parts.push(`${health.words_per_sec} words/s`);
  if (warnings.length) {
    parts.push('Warning: ' + warnings.map(escHtml).join('; '));
  }
  const sounds = d.sounds || {};
  for (const [key, sound] of Object.entries(sounds)) {
    const label = key === 'before_sound' ? 'before sound' : 'after sound';
    parts.push(sound.ok ? `${label} OK` : `${label}: ${escHtml(sound.error || 'not found')}`);
    if (!sound.ok) el.className = 'routing-test-result warn';
  }
  el.innerHTML = parts.join(' · ');
}

function routingLogTime(ts) {
  if (!ts) return '--:--:--';
  const d = new Date(ts);
  if (Number.isNaN(d.getTime())) return String(ts).slice(11, 19) || '--:--:--';
  return d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'});
}

function routingLogBadge(item) {
  const status = String(item.status || item.kind || 'log');
  if (item.kind === 'test' && status === 'matched') return 'test ok';
  if (item.kind === 'test' && status === 'no_match') return 'test miss';
  return status.replace(/_/g, ' ');
}

function routingLogMeta(item) {
  const parts = [];
  if (item.backend) parts.push(item.backend);
  if (item.language) parts.push('lang ' + item.language);
  if (item.response_format) parts.push(item.response_format);
  if (item.duration != null) parts.push(Number(item.duration).toFixed(2) + 's');
  if (item.bytes != null) parts.push(Math.round(Number(item.bytes) / 1024) + ' KB');
  if (item.sounds && Array.isArray(item.sounds) && item.sounds.length) parts.push('sounds ' + item.sounds.join(','));
  if (item.route_id) parts.push('route ' + item.route_id);
  if (item.client) parts.push(item.client);
  return parts.join(' · ');
}

let _routingLogItems = [];
let _routingLogFilter = 'all';

function routingLogPassesFilter(item) {
  const status = String(item?.status || '').toLowerCase();
  const isError = status === 'error' || Boolean(item?.error);
  const isNoMatch = status === 'no_match' || item?.matched === false;
  if (_routingLogFilter === 'error') return isError;
  if (_routingLogFilter === 'no_match') return isNoMatch;
  if (_routingLogFilter === 'attention') return isError || isNoMatch;
  return true;
}

function renderCurrentRoutingLog() {
  renderRoutingLog(_routingLogItems.filter(routingLogPassesFilter));
}

function renderRoutingLog(items = []) {
  const el = $('routing-log-list');
  if (!el) return;
  if (!items.length) {
    const filtered = _routingLogItems.length && _routingLogFilter !== 'all';
    el.innerHTML = `<div class="routing-log-empty">${filtered ? 'No routing log entries match this filter.' : 'No routing log entries yet. Test a route or send a TTS request through the Creator proxy.'}</div>`;
    return;
  }
  el.innerHTML = items.map(item => {
    const status = String(item.status || 'log').replace(/[^a-z0-9_-]/gi, '_');
    const requested = item.requested_voice || '-';
    const routed = item.routed_voice || '-';
    const voice = requested === routed ? requested : `${requested} → ${routed}`;
    const text = item.error ? `Error: ${item.error}` : (item.text_preview || '');
    return `
      <div class="routing-log-entry status-${escHtml(status)}">
        <div class="log-time">${escHtml(routingLogTime(item.ts))}</div>
        <div class="routing-log-badge">${escHtml(routingLogBadge(item))}</div>
        <div class="log-app" title="${escHtml(item.app || '-')}">${escHtml(item.app || '-')}</div>
        <div class="log-voice" title="${escHtml(voice)}">${escHtml(voice)}</div>
        <div class="log-meta" title="${escHtml(routingLogMeta(item))}">${escHtml(routingLogMeta(item) || '-')}</div>
        <div class="log-text" title="${escHtml(text)}">${escHtml(text || '-')}</div>
      </div>`;
  }).join('');
}

async function loadRoutingLog() {
  const el = $('routing-log-list');
  if (!el) return;
  try {
    const r = await fetch('/api/tts-routing-log?limit=80');
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
    const d = await r.json();
    _routingLogItems = Array.isArray(d.items) ? d.items : [];
    renderCurrentRoutingLog();
  } catch(e) {
    el.innerHTML = `<div class="routing-log-empty" style="color:var(--red)">Routing log unavailable: ${escHtml(e.message)}</div>`;
  }
}

async function clearRoutingLog() {
  const btn = $('routing-log-clear-btn');
  if (btn) btn.disabled = true;
  try {
    const r = await fetch('/api/tts-routing-log', {method:'DELETE'});
    if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
    _routingLogItems = [];
    renderRoutingLog([]);
    toast('Routing log cleared', 'success');
  } catch(e) {
    toast('Clear log failed: ' + e.message, 'error');
  } finally {
    if (btn) btn.disabled = false;
  }
}

async function testRouting() {
  readRoutingForm();
  const btn = $('routing-test-btn');
  const el = $('routing-test-result');
  btn.disabled = true;
  el.className = 'routing-test-result';
  el.textContent = 'Testing route...';
  try {
    const r = await fetch('/api/tts-route-test', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        app: $('routing-test-app').value.trim() || 'Open WebUI',
        voice: $('routing-test-voice').value.trim() || 'default',
        input: $('routing-test-text').value.trim(),
      }),
    });
    if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
    renderRouteTestResult(await r.json());
    loadRoutingLog();
  } catch(e) {
    el.className = 'routing-test-result warn';
    el.textContent = 'Route test failed: ' + e.message;
  } finally {
    btn.disabled = false;
  }
}

async function uploadRouteSoundForRow(row, target) {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'audio/*';
  input.multiple = false;
  input.onchange = async () => {
    if (!input.files || !input.files.length) return;
    const btn = row.querySelector(`.route-sound-upload[data-target="${target}"]`);
    const field = row.querySelector(target === 'before' ? '.route-before-sound' : '.route-after-sound');
    if (btn) btn.disabled = true;
    try {
      const fd = new FormData();
      fd.append('file', input.files[0]);
      const r = await fetch('/api/route-sounds/upload', { method:'POST', body:fd });
      if (!r.ok) { const e = await r.json().catch(()=>({})); throw new Error(e.detail || r.statusText); }
      const d = await r.json();
      field.value = d.path || '';
      await loadRouteSounds();
      readRoutingForm();
      renderRoutingList();
      toast(`${target === 'before' ? 'Before' : 'After'} sound uploaded`, 'success');
      status(`Uploaded route sound: ${d.path}`);
    } catch(e) {
      toast('Sound upload failed: ' + e.message, 'error');
      status('Sound upload failed');
    } finally {
      if (btn) btn.disabled = false;
    }
  };
  input.click();
}

$('routing-refresh-btn')?.addEventListener('click', loadRoutingTab);
$('routing-add-btn')?.addEventListener('click', () => {
  readRoutingForm();
  _ttsRoutes.push(newRoute());
  renderRoutingList();
});
$('routing-add-openwebui-btn')?.addEventListener('click', () => {
  readRoutingForm();
  const voices = activeVoiceIds();
  const firstByLang = lang => voices.find(v => v.toUpperCase().startsWith(lang + '_')) || '';
  _ttsRoutes.push(newRoute('Open WebUI', 'default', 'EN', firstByLang('EN')));
  _ttsRoutes.push(newRoute('Open WebUI', 'default', 'DE', firstByLang('DE')));
  renderRoutingList();
});
$('routing-save-btn')?.addEventListener('click', saveRoutingTab);
$('routing-test-btn')?.addEventListener('click', testRouting);
$('routing-log-refresh-btn')?.addEventListener('click', loadRoutingLog);
$('routing-log-clear-btn')?.addEventListener('click', clearRoutingLog);
$('routing-log-filter')?.addEventListener('change', (e) => {
  _routingLogFilter = e.target.value || 'all';
  renderCurrentRoutingLog();
});
$('routing-list')?.addEventListener('change', e => {
  const picker = e.target.closest('.route-sound-picker');
  if (!picker) return;
  const row = picker.closest('.routing-row');
  const field = row.querySelector(picker.dataset.target === 'before' ? '.route-before-sound' : '.route-after-sound');
  if (field) field.value = picker.value || '';
  readRoutingForm();
});
$('routing-list')?.addEventListener('click', e => {
  const pickBtn = e.target.closest('.route-sound-pick');
  if (pickBtn) {
    const row = pickBtn.closest('.routing-row');
    openRouteSoundBrowser(row, pickBtn.dataset.target);
    return;
  }
  const uploadBtn = e.target.closest('.route-sound-upload');
  if (uploadBtn) {
    const row = uploadBtn.closest('.routing-row');
    uploadRouteSoundForRow(row, uploadBtn.dataset.target);
    return;
  }
  const btn = e.target.closest('.routing-delete');
  if (!btn) return;
  readRoutingForm();
  const row = btn.closest('.routing-row');
  _ttsRoutes.splice(Number(row.dataset.index), 1);
  renderRoutingList();
});

$('routing-sound-search')?.addEventListener('input', debounce(renderRouteSoundBrowser, 120));
$('routing-sound-refresh-btn')?.addEventListener('click', async () => {
  await loadRouteSounds();
  renderRouteSoundBrowser();
});
$('routing-sound-close-btn')?.addEventListener('click', closeRouteSoundBrowser);
$('routing-sound-list')?.addEventListener('click', e => {
  const item = e.target.closest('.routing-sound-item');
  if (!item) return;
  const path = item.dataset.path || '';
  const playBtn = e.target.closest('.sound-play');
  if (playBtn) {
    playRouteSound(path, playBtn);
    return;
  }
  if (e.target.closest('.sound-use-current')) {
    useRouteSound(path, _routeSoundPickerTarget?.target || 'before');
  }
});

