/**
 * VMO OpenAI TTS HD Client
 * 
 * Simplified client for OpenAI Text-to-Speech
 * Works with REST API proxy (no direct API key exposure)
 * 
 * @package VMO_AI_Website_Builder
 * @since 1.0.0
 */

(function(){
  'use strict';

  // Abort controller for current TTS network activity (enables true barge-in)
  let VMO_TTS_ABORT = null;


  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, Math.max(0, ms || 0)));
  }

  function estimateTtsPlaybackMs(value) {
    const text = String(value || '').trim();
    if (!text) return 120000;
    const words = text.split(/\s+/).filter(Boolean).length;
    const spokenMs = Math.max((words / 2.6) * 1000, text.length * 35);
    return Math.max(120000, Math.min(45 * 60 * 1000, Math.ceil(spokenMs + 60000)));
  }

  // Global function to speak text
  async function vmoSpeak(text, voice='ash', format='mp3', speed=1.0, options){
    // Back-compat: vmoSpeak(text, voice, format, options)
    let opts = options;
    let spd = speed;
    if (speed && typeof speed === 'object' && !Array.isArray(speed)) {
      opts = speed;
      spd = 1.0;
    }
    opts = (opts && typeof opts === 'object') ? opts : {};

    if (!text || !text.trim()) {
      console.warn('VMO TTS: No text provided');
      return null;
    }

    // VMO_TTS_PRONOUNCE_JERREME_v1
    // Keep the UI spelling as "Jerreme" while forcing spoken TTS pronunciation
    // to "Jeremy". This applies to Jarvis, the consultant, and all client voices.
    function prepareTextForSpeech(value) {
      return String(value || '')
        .replace(/\bJerreme\b/g, 'Jeremy')
        .replace(/\bjerreme\b/g, 'Jeremy');
    }
    const speechText = prepareTextForSpeech(text);

    // Create fresh abort controller for this speak
    try { if (VMO_TTS_ABORT) { VMO_TTS_ABORT.abort(); } } catch(_) {}
    VMO_TTS_ABORT = new AbortController();

    // Ensure only one TTS stream plays at a time
    try {
      const existing = document.getElementById('vmo-tts-container');
      if (existing) {
        existing.querySelectorAll('audio').forEach(a => {
          try { a.pause(); } catch(_) {}
          try { a.currentTime = 0; } catch(_) {}
          try { a.removeAttribute('src'); } catch(_) {}
          try { a.load(); } catch(_) {}
          try { a.remove(); } catch(_) {}
        });
      }
      // If something had marked the coordinator as speaking, clear it now.
      try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
    } catch(_) {}

    const restUrl = (window.wpApiSettings?.root || '/wp-json/') + 'vmo/v1/tts/speak';
    const nonce = window.wpApiSettings?.nonce || '';

    const mimeMap = {
      mp3: 'audio/mpeg',
      // OpenAI "opus" comes in an Ogg container; use the widely supported type
      opus: 'audio/ogg; codecs=opus',
      aac: 'audio/aac',
      flac: 'audio/flac',
      wav: 'audio/wav',
      pcm: 'audio/pcm'
    };

    function decodeBase64ToArrayBuffer(base64) {
      const clean = String(base64 || '').replace(/^data:audio\/[a-z0-9.+-]+;base64,/i, '').replace(/\s+/g, '');
      const bin = atob(clean);
      const len = bin.length;
      const bytes = new Uint8Array(len);
      for (let i = 0; i < len; i++) bytes[i] = bin.charCodeAt(i);
      return bytes.buffer;
    }

    function textPreviewFromBuffer(buffer, limit = 512) {
      try {
        const bytes = new Uint8Array(buffer || new ArrayBuffer(0)).slice(0, limit);
        return new TextDecoder('utf-8', { fatal: false }).decode(bytes).trim();
      } catch (_) { return ''; }
    }

    function sniffAudioMime(buffer, fallbackMime) {
      const b = new Uint8Array(buffer || new ArrayBuffer(0));
      if (b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 && b[8] === 0x57 && b[9] === 0x41 && b[10] === 0x56 && b[11] === 0x45) return 'audio/wav';
      if (b.length >= 4 && b[0] === 0x4f && b[1] === 0x67 && b[2] === 0x67 && b[3] === 0x53) return 'audio/ogg; codecs=opus';
      if (b.length >= 4 && b[0] === 0x66 && b[1] === 0x4c && b[2] === 0x61 && b[3] === 0x43) return 'audio/flac';
      if (b.length >= 3 && b[0] === 0x49 && b[1] === 0x44 && b[2] === 0x33) return 'audio/mpeg';
      if (b.length >= 2 && b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return 'audio/mpeg';
      if (b.length >= 2 && b[0] === 0xff && (b[1] & 0xf6) === 0xf0) return 'audio/aac';
      return ''; // no magic match: do not hand fake audio to <audio>, it will DEMUXER_ERROR
    }

    function findAudioMagicOffset(buffer) {
      const b = new Uint8Array(buffer || new ArrayBuffer(0));
      const max = Math.min(8192, b.length);
      for (let i = 0; i < max; i++) {
        if (i + 12 <= b.length && b[i] === 0x52 && b[i + 1] === 0x49 && b[i + 2] === 0x46 && b[i + 3] === 0x46 && b[i + 8] === 0x57 && b[i + 9] === 0x41 && b[i + 10] === 0x56 && b[i + 11] === 0x45) return i;
        if (i + 4 <= b.length && b[i] === 0x4f && b[i + 1] === 0x67 && b[i + 2] === 0x67 && b[i + 3] === 0x53) return i;
        if (i + 4 <= b.length && b[i] === 0x66 && b[i + 1] === 0x4c && b[i + 2] === 0x61 && b[i + 3] === 0x43) return i;
        if (i + 3 <= b.length && b[i] === 0x49 && b[i + 1] === 0x44 && b[i + 2] === 0x33) return i;
        if (i + 2 <= b.length && b[i] === 0xff && (b[i + 1] & 0xe0) === 0xe0) return i;
        if (i + 2 <= b.length && b[i] === 0xff && (b[i + 1] & 0xf6) === 0xf0) return i;
      }
      return -1;
    }

    function normalizeAudioPayload(buffer, responseContentType, requestedFmt) {
      let audioBuffer = buffer;
      let contentType = String(responseContentType || '').toLowerCase();
      const preview = textPreviewFromBuffer(audioBuffer, 600);

      // VMO_TTS_MISLABELED_BINARY_AUDIO_RECOVERY_v1
      // The Kinsta master proxy can legitimately return raw audio while an
      // intermediate REST/WP layer labels it text/plain or application/octet-stream.
      // Never treat a binary-looking payload as text merely because the header is
      // wrong. First look for audio magic bytes; if present, trust the bytes.
      let earlyMagicOffset = findAudioMagicOffset(audioBuffer);
      if (earlyMagicOffset > 0) {
        audioBuffer = audioBuffer.slice(earlyMagicOffset);
        earlyMagicOffset = 0;
        console.warn('VMO TTS: Stripped leading bytes before audio container', { bytes: audioBuffer.byteLength });
      }
      const earlySniffed = sniffAudioMime(audioBuffer, contentType || (mimeMap[requestedFmt] || 'audio/mpeg'));
      if (earlySniffed && !contentType.includes('json') && !contentType.includes('text/html')) {
        return new Blob([audioBuffer], { type: earlySniffed });
      }

      const previewLooksBinary = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffd]/.test(preview);
      const contentTypeSaysStructuredText = contentType.includes('json') || contentType.includes('text/html');
      const looksText = preview.startsWith('{') || preview.startsWith('[') || preview.startsWith('<!doctype') || preview.startsWith('<html') || contentTypeSaysStructuredText || (contentType.includes('text/plain') && !previewLooksBinary);

      if (!looksText && previewLooksBinary && audioBuffer && audioBuffer.byteLength > 1000) {
        const fallbackType = (contentType.includes('audio/') ? contentType : (mimeMap[requestedFmt] || 'audio/mpeg'));
        console.warn('VMO TTS: Accepting mislabeled binary audio payload', { bytes: audioBuffer.byteLength, type: fallbackType });
        return new Blob([audioBuffer], { type: fallbackType });
      }

      if (looksText) {
        let parsed = null;
        try { parsed = JSON.parse(new TextDecoder('utf-8', { fatal: false }).decode(new Uint8Array(audioBuffer))); } catch (_) {}
        const audio64 = parsed && (parsed.audio || parsed.data?.audio || parsed.data?.audio_base64 || parsed.audio_base64);
        if (audio64) {
          audioBuffer = decodeBase64ToArrayBuffer(audio64);
          const fmt = (parsed.format || parsed.data?.format || requestedFmt || 'mp3').toString().toLowerCase();
          contentType = mimeMap[fmt] || contentType || 'audio/mpeg';
          console.log('VMO TTS: Decoded base64 audio proxy response', { format: fmt, bytes: audioBuffer.byteLength });
        } else {
          const message = parsed?.message || parsed?.error?.message || parsed?.data?.message || preview.substring(0, 160) || 'TTS returned non-audio response';
          const err = new Error(message);
          err.isInvalidAudio = true;
          throw err;
        }
      }

      if (!audioBuffer || audioBuffer.byteLength < 128) {
        const err = new Error('TTS returned empty or tiny audio payload');
        err.isInvalidAudio = true;
        throw err;
      }

      const magicOffset = findAudioMagicOffset(audioBuffer);
      if (magicOffset > 0) {
        audioBuffer = audioBuffer.slice(magicOffset);
        console.warn('VMO TTS: Stripped leading non-audio bytes from proxy response', { offset: magicOffset, bytes: audioBuffer.byteLength });
      }

      let sniffed = sniffAudioMime(audioBuffer, contentType || (mimeMap[requestedFmt] || 'audio/mpeg'));

      // VMO_TTS_RAW_BASE64_AUDIO_RECOVERY_v2
      // Some client/master proxy paths can return raw base64 while preserving an
      // audio/* content-type. Decode it once before declaring the payload invalid.
      if (!sniffed) {
        const maybeBase64 = preview.replace(/\s+/g, '');
        if (/^(?:data:audio\/[a-z0-9.+-]+;base64,)?[A-Za-z0-9+/]+={0,2}$/i.test(maybeBase64) && maybeBase64.length > 256) {
          try {
            const decoded = decodeBase64ToArrayBuffer(maybeBase64);
            const decodedMime = sniffAudioMime(decoded, contentType || (mimeMap[requestedFmt] || 'audio/mpeg'));
            if (decodedMime) {
              audioBuffer = decoded;
              sniffed = decodedMime;
              console.log('VMO TTS: Decoded raw base64 audio proxy response', { bytes: audioBuffer.byteLength, type: sniffed });
            }
          } catch (_) {}
        }
      }

      if (!sniffed) {
        const err = new Error('TTS payload is not a playable audio container');
        err.isInvalidAudio = true;
        throw err;
      }

      return new Blob([audioBuffer], { type: sniffed });
    }

    // Prefer broadly supported formats first; AAC (ADTS) is poorly supported in <audio>
    const fallbackOrder = ['mp3','opus','wav'];

    const defaultFetchTimeoutMs = Math.max(30000, Math.min(180000, 25000 + text.trim().length * 35));
    const maxAttempts = Number.isFinite(opts.maxAttempts) ? Math.max(1, opts.maxAttempts) : 1;
    const perFetchTimeoutMs = Number.isFinite(opts.timeoutMs) ? Math.max(1000, opts.timeoutMs) : defaultFetchTimeoutMs;
    const playbackSignalTimeoutMs = Number.isFinite(opts.playbackSignalTimeoutMs)
      ? Math.max(30000, opts.playbackSignalTimeoutMs)
      : estimateTtsPlaybackMs(text);
    // GET-streaming is now opt-in. It was causing 5s+8s startup stalls before the reliable POST path.
    const allowStreaming = opts.allowStreaming === true;
    function getClientProxyConfig() {
      const cfg = window.vmoClientProxy || {};
      const base = String(cfg.master_proxy_url || '').replace(/\/+$/, '');
      const apiBase = base.replace(/\/proxy$/i, '');
      return {
        enabled: !!(cfg.enabled && cfg.direct_master_voice && base && cfg.license_key && cfg.installation_id),
        base,
        apiBase,
        license: String(cfg.license_key || ''),
        installation: String(cfg.installation_id || '')
      };
    }

    function makeRequestId(prefix) {
      try { if (crypto && crypto.randomUUID) return prefix + '_' + crypto.randomUUID(); } catch (_) {}
      return prefix + '_' + Date.now() + '_' + Math.random().toString(16).slice(2);
    }

    async function playAudioBlob(blob, sourceLabel, controller) {
      const url = URL.createObjectURL(blob);
      console.log(`VMO TTS: Audio via ${sourceLabel} (${blob.size} bytes, ${blob.type})`);

      const audio = document.createElement('audio');
      audio.src = url;
      audio.preload = 'auto';
      audio.autoplay = false;
      audio.muted = false;
      audio.volume = 1.0;
      audio.playsInline = true;
      audio.setAttribute('playsinline', '');
      audio.controls = false;

      audio.vmoStop = () => {
        try { controller && controller.abort(); } catch(_) {}
        try { audio.pause(); } catch(_) {}
        try { audio.currentTime = 0; } catch(_) {}
        try { audio.removeAttribute('src'); } catch(_) {}
        try { audio.load(); } catch(_) {}
        try { audio.remove(); } catch(_) {}
        try { URL.revokeObjectURL(url); } catch(_) {}
      };

      let container = document.getElementById('vmo-tts-container');
      if (!container) {
        container = document.createElement('div');
        container.id = 'vmo-tts-container';
        container.style.cssText = 'position:fixed;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;bottom:0;right:0;';
        document.body.appendChild(container);
      }
      container.appendChild(audio);
      try { audio.load(); } catch(_) {}

      const cleanup = () => {
        try { URL.revokeObjectURL(url); } catch(_) {}
        try { audio.remove(); } catch(_) {}
      };
      audio.addEventListener('ended', cleanup, { once: true });
      audio.addEventListener('error', cleanup, { once: true });

      await new Promise((resolve) => {
        let resolved = false;
        const done = () => { if (!resolved) { resolved = true; resolve(); } };
        audio.addEventListener('loadedmetadata', done, { once: true });
        audio.addEventListener('canplaythrough', done, { once: true });
        setTimeout(done, 2000);
      });

      try {
        try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStart(sourceLabel); } catch(_) {}
        let stopSignaled = false;
        const signalStop = () => {
          if (stopSignaled) return;
          stopSignaled = true;
          try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
        };
        const playPromise = audio.play();
        if (playPromise !== undefined) await playPromise;
        audio.addEventListener('ended', signalStop, { once: true });
        audio.addEventListener('error', signalStop, { once: true });
        audio.addEventListener('pause', signalStop, { once: true });
        setTimeout(signalStop, playbackSignalTimeoutMs);
        return audio;
      } catch (err) {
        try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
        throw err;
      }
    }

    async function tryDirectMasterTTS(fmt) {
      const cfg = getClientProxyConfig();
      if (!cfg.enabled) return null;
      const controller = new AbortController();
      const timeoutId = setTimeout(() => { try { controller.abort(); } catch(_){} }, perFetchTimeoutMs);
      try {
        try { VMO_TTS_ABORT.signal.addEventListener('abort', () => { try { controller.abort(); } catch(_){}; }, { once: true }); } catch(_) {}
        const target = cfg.apiBase + '/voice/tts';
        const resp = await fetch(target, {
          method: 'POST',
          mode: 'cors',
          credentials: 'omit',
          headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            'X-VMO-License': cfg.license,
            'X-VMO-Installation': cfg.installation,
            'X-VMO-Operation': 'tts',
            'X-VMO-Operation-Multiplier': String(Math.max(1, Math.ceil(String(speechText || '').length / 1000))),
            'X-VMO-Request-ID': makeRequestId('tts_direct_json')
          },
          body: JSON.stringify({
            model: 'tts-1',
            input: speechText.trim(),
            voice: voice,
            response_format: fmt,
            speed: Number(spd || 1.0) || 1.0
          }),
          signal: controller.signal
        });
        clearTimeout(timeoutId);
        const raw = await resp.text();
        let json = null;
        try { json = raw ? JSON.parse(raw) : null; } catch (_) {}
        if (!resp.ok || !json || json.success === false) {
          const msg = (json && (json.message || (json.data && json.data.message))) || raw || resp.statusText || '';
          throw Object.assign(new Error('Direct master TTS failed: ' + resp.status + ' ' + String(msg).slice(0, 300)), { status: resp.status });
        }
        const data = json.data || json;
        if (!data.audio_base64) throw new Error('Direct master TTS returned no audio_base64');
        const buffer = decodeBase64ToArrayBuffer(data.audio_base64);
        const blob = normalizeAudioPayload(buffer, (data.mime_type || mimeMap[fmt] || 'audio/mpeg'), data.format || fmt);
        if (blob.size >= 100) {
          return await playAudioBlob(blob, 'Kinsta master voice proxy', controller);
        }
      } catch (err) {
        clearTimeout(timeoutId);
        console.warn('VMO TTS: direct master voice proxy failed, falling back to local client route', { message: err && err.message ? err.message : String(err), status: err && err.status });
      }
      return null;
    }


    async function attemptSpeak(fmt, tried = new Set()){
      tried.add(fmt);
      try {
        console.log('VMO TTS: Requesting speech synthesis...', {text: speechText.substring(0, 50) + '...', voice, format: fmt});

        // VMO_CLIENT_DIRECT_MASTER_TTS_v1
        // On brand-new QuickLaunch clients, the WordPress page may load while the
        // tenant admin-ajax/REST proxy still returns Cloudflare 502/503 under TTS
        // load. In client mode, bypass that local PHP path and speak through the
        // Kinsta master proxy directly with the client license headers.
        const directAudio = await tryDirectMasterTTS(fmt);
        if (directAudio) return directAudio;
        
        // Try STREAMING first via admin-ajax to enable progressive playback
        // SKIP streaming for long text (>1500 chars) — GET URLs exceed server limits
        // and cause HTML error responses. Fall through to POST-based REST instead.
        const textLen = speechText.trim().length;
        if (allowStreaming && textLen <= 1500) try {
          const ajaxUrl = (window.vmo_ajax?.ajaxurl || '/wp-admin/admin-ajax.php');
          const streamUrl = ajaxUrl + `?action=vmo_tts_stream&voice=${encodeURIComponent(voice)}&format=${encodeURIComponent(fmt)}&text=${encodeURIComponent(text.trim())}`;

          for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            // OPTIMIZATION: Removed redundant fetch pre-check that was causing double-generation latency.
            // We trust the audio element to load the stream and handle errors via events.
            
            const audioStream = document.createElement('audio');
            audioStream.preload = 'auto';
            audioStream.autoplay = false;
            audioStream.muted = false;
            audioStream.volume = 1.0;
            audioStream.playsInline = true;
            audioStream.setAttribute('playsinline', '');
            audioStream.src = streamUrl;

            // Provide a stop hook so coordinator barge-in can abort immediately.
            audioStream.vmoStop = () => {
              try { if (VMO_TTS_ABORT) { VMO_TTS_ABORT.abort(); } } catch(_) {}

              try { audioStream.pause(); } catch(_) {}
              try { audioStream.currentTime = 0; } catch(_) {}
              try { audioStream.removeAttribute('src'); } catch(_) {}
              try { audioStream.load(); } catch(_) {}
              try { audioStream.remove(); } catch(_) {}
            };

            let container = document.getElementById('vmo-tts-container');
            if (!container) {
              container = document.createElement('div');
              container.id = 'vmo-tts-container';
              container.style.cssText = 'position:fixed;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;bottom:0;right:0';
              document.body.appendChild(container);
            }
            container.appendChild(audioStream);

            const startupTimeout = Math.min(12000, 5000 + (attempt - 1) * 3000);
            const started = await new Promise((resolve) => {
              let resolved = false;
              const done = (ok) => { if (!resolved) { resolved = true; cleanup(); resolve(ok); } };
              const onPlaying = () => done(true);
              const onError = () => done(false);
              const cleanup = () => {
                audioStream.removeEventListener('playing', onPlaying);
                audioStream.removeEventListener('error', onError);
              };
              audioStream.addEventListener('playing', onPlaying);
              audioStream.addEventListener('error', onError);
              setTimeout(() => done(false), startupTimeout);
              try {
                const p = audioStream.play();
                if (p && typeof p.catch === 'function') {
                  p.catch(err => {
                    if (err && err.name === 'AbortError') return;
                    console.warn('VMO TTS: stream play() failed', err);
                  });
                }
              } catch(_) {}
            });

            if (started) {
              try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStart('stream'); } catch(_) {}
              const onStop = () => { try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {} };
              audioStream.addEventListener('ended', onStop, { once: true });
              audioStream.addEventListener('error', onStop, { once: true });
              audioStream.addEventListener('pause', onStop, { once: true });
              return audioStream;
            }

            console.warn('VMO TTS: Streaming did not start, retrying…', { attempt, startupTimeout });
            try { audioStream.vmoStop && audioStream.vmoStop('startup-timeout'); } catch(_) {}
            await sleep(250 * attempt);
          }
        } catch(_streamErr) {
          // Ignore and fall back to admin-ajax POST / REST
        }

        // ─── ADMIN-AJAX POST FALLBACK ───────────────────────────────
        // Uses cookie auth (no REST nonce dependency, no permalink routing).
        // Works for ALL text lengths (no URL limit like GET streaming).
        // This is the most reliable path when REST API returns HTML.
        try {
          const ajPostUrl = (window.vmo_ajax?.ajaxurl || window.vmoAjax?.ajaxurl || '/wp-admin/admin-ajax.php');
          const ajaxNonce = window.vmo_ajax?.nonce || window.vmoAjax?.nonce || window.vmo_ajax?.security || window.vmoAjax?.security || '';
          const ajController = new AbortController();
          const ajTimeoutId = setTimeout(() => { try { ajController.abort(); } catch(_){} }, perFetchTimeoutMs);
          try {
            VMO_TTS_ABORT.signal.addEventListener('abort', () => {
              try { ajController.abort(); } catch(_) {}
            }, { once: true });
          } catch(_) {}

          const ajResp = await fetch(ajPostUrl, {
            method: 'POST',
            credentials: 'same-origin',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
              action: 'vmo_tts_stream',
              security: ajaxNonce,
              nonce: ajaxNonce,
              _ajax_nonce: ajaxNonce,
              text: speechText.trim(),
              voice: voice,
              format: fmt
            }).toString(),
            signal: ajController.signal
          });

          clearTimeout(ajTimeoutId);
          const ajCt = (ajResp.headers.get('Content-Type') || '').toLowerCase();
          if (ajResp.ok && ajCt.startsWith('audio/')) {
            const buffer = await ajResp.arrayBuffer();
            const blob = normalizeAudioPayload(buffer, ajCt, fmt);
            if (blob.size >= 100) {
              const url = URL.createObjectURL(blob);
              console.log(`VMO TTS: Audio via admin-ajax POST (${blob.size} bytes, ${blob.type})`);

              const audio = document.createElement('audio');
              audio.src = url;
              audio.preload = 'auto';
              audio.autoplay = false;
              audio.muted = false;
              audio.volume = 1.0;
              audio.playsInline = true;
              audio.setAttribute('playsinline', '');
              audio.controls = false;

              audio.vmoStop = () => {
                try { ajController.abort(); } catch(_) {}
                try { audio.pause(); } catch(_) {}
                try { audio.currentTime = 0; } catch(_) {}
                try { audio.removeAttribute('src'); } catch(_) {}
                try { audio.load(); } catch(_) {}
                try { audio.remove(); } catch(_) {}
                try { URL.revokeObjectURL(url); } catch(_) {}
              };

              let container = document.getElementById('vmo-tts-container');
              if (!container) {
                container = document.createElement('div');
                container.id = 'vmo-tts-container';
                container.style.cssText = 'position:fixed;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;bottom:0;right:0;';
                document.body.appendChild(container);
              }
              container.appendChild(audio);
              try { audio.load(); } catch(_e) {}

              const cleanup = () => {
                try { URL.revokeObjectURL(url); } catch(_) {}
                try { audio.remove(); } catch(_) {}
              };
              audio.addEventListener('ended', cleanup, { once: true });
              audio.addEventListener('error', cleanup, { once: true });

              await new Promise((resolve) => {
                let resolved = false;
                const done = () => { if (!resolved) { resolved = true; resolve(); } };
                audio.addEventListener('loadedmetadata', done, { once: true });
                audio.addEventListener('canplaythrough', done, { once: true });
                setTimeout(done, 2000);
              });

              try {
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStart('buffer'); } catch(_) {}
                let stopSignaled = false;
                const signalStop = () => {
                  if (stopSignaled) return;
                  stopSignaled = true;
                  try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
                };
                const playPromise = audio.play();
                if (playPromise !== undefined) await playPromise;
                audio.addEventListener('ended', signalStop, { once: true });
                audio.addEventListener('error', signalStop, { once: true });
                audio.addEventListener('pause', signalStop, { once: true });
                setTimeout(signalStop, playbackSignalTimeoutMs);
                return audio;
              } catch (err) {
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
                throw err;
              }
            } else {
              console.warn('VMO TTS: admin-ajax POST returned small response (' + blob.size + 'B), falling through to REST');
            }
          } else {
            let snippet = '';
            try { snippet = (await ajResp.clone().text()).substring(0, 150); } catch(_) {}
            console.warn('VMO TTS: admin-ajax POST failed:', ajResp.status, ajCt, snippet);
            if ([500, 502, 503, 504, 521, 522, 523, 524].indexOf(parseInt(ajResp.status || 0, 10)) !== -1) {
              const err = new Error(`TTS origin/admin-ajax warming (${ajResp.status})` + (ajCt ? ` [type=${ajCt}]` : ''));
              err.status = parseInt(ajResp.status || 0, 10);
              err.contentType = ajCt;
              throw err;
            }
          }
        } catch(_ajErr) {
          if (_ajErr && _ajErr.name === 'AbortError') throw _ajErr;
          console.warn('VMO TTS: admin-ajax POST error:', _ajErr.message);
        }

        // ─── REST POST FALLBACK ─────────────────────────────────────
        // Uses X-WP-Nonce header; requires working REST API routing.
        let lastErr;
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
          const controller = new AbortController();
          const timeoutId = setTimeout(() => {
            try { controller.abort(); } catch(_) {}
          }, perFetchTimeoutMs);

          try {
            // Tie the per-request abort controller to the global TTS abort controller
            // so barge-in can stop an in-flight request instantly.
            try {
              VMO_TTS_ABORT.signal.addEventListener('abort', () => {
                try { controller.abort(); } catch(_) {}
              }, { once: true });
            } catch(_) {}

            const response = await fetch(restUrl, {
              method: 'POST',
              credentials: 'same-origin',  // Important: send cookies for WordPress auth
              headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': nonce
              },
              body: JSON.stringify({
                text: speechText.trim(),
                voice,
                format: fmt,
                speed: spd
              }),
              signal: controller.signal
            });

            // Quick sanity: require audio/* content-type
            const respType = (response.headers.get('Content-Type') || '').toLowerCase();
            if (!response.ok || !respType.startsWith('audio/')) {
              let errorText = '';
              try { errorText = await response.clone().text(); } catch(_e) {}
              let msg = '';
              try { const j = JSON.parse(errorText); msg = j?.message || j?.error?.message || ''; } catch(_e) {}
              const errMsg = `TTS failed (${response.status} ${response.statusText})` + (msg ? `: ${msg}` : '') + (respType ? ` [type=${respType}]` : '');
              const err = new Error(errMsg);
              err.status = response.status;
              err.contentType = respType;
              throw err;
            }

            const buffer = await response.arrayBuffer();
            const blob = normalizeAudioPayload(buffer, respType, fmt);
            const url = URL.createObjectURL(blob);

            console.log(`VMO TTS: Audio generated successfully (${blob.size} bytes, ${blob.type})`);

            const audio = document.createElement('audio');
            audio.src = url;
            audio.preload = 'auto';
            audio.autoplay = false;
            audio.muted = false;
            audio.volume = 1.0;
            audio.playsInline = true;
            audio.setAttribute('playsinline', '');
            audio.controls = false;

            // Attach cancellation hook for barge-in
            audio.vmoStop = () => {
              try { controller.abort(); } catch(_) {}
              try { audio.pause(); } catch(_) {}
              try { audio.currentTime = 0; } catch(_) {}
              try { audio.removeAttribute('src'); } catch(_) {}
              try { audio.load(); } catch(_) {}
              try { audio.remove(); } catch(_) {}
              try { URL.revokeObjectURL(url); } catch(_) {}
            };

            let container = document.getElementById('vmo-tts-container');
            if (!container) {
              container = document.createElement('div');
              container.id = 'vmo-tts-container';
              container.style.cssText = 'position:fixed;width:1px;height:1px;overflow:hidden;opacity:0;pointer-events:none;bottom:0;right:0;';
              document.body.appendChild(container);
            }
            container.appendChild(audio);

            try { audio.load(); } catch(_e) {
              console.warn('VMO TTS: audio.load() failed', _e);
            }

            const cleanup = () => {
              clearTimeout(timeoutId);
              try { URL.revokeObjectURL(url); } catch(_) {}
              try { audio.remove(); } catch(_) {}
            };
            audio.addEventListener('ended', cleanup, { once: true });
            audio.addEventListener('error', (e) => {
              console.error('VMO TTS: <audio> element error', { code: audio.error?.code, message: audio.error?.message });
              cleanup();
            }, { once: true });

            await new Promise((resolve) => {
              let resolved = false;
              const done = () => { if (!resolved) { resolved = true; resolve(); } };
              const onMeta = () => { audio.removeEventListener('loadedmetadata', onMeta); done(); };
              const onCPT = () => { audio.removeEventListener('canplaythrough', onCPT); done(); };
              const onPlaying = () => { audio.removeEventListener('playing', onPlaying); done(); };
              audio.addEventListener('loadedmetadata', onMeta);
              audio.addEventListener('canplaythrough', onCPT);
              audio.addEventListener('playing', onPlaying);
              setTimeout(done, 2000);
            });

            try {
              try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStart('buffer'); } catch(_) {}

              let stopSignaled = false;
              const signalStop = () => {
                if (stopSignaled) return;
                stopSignaled = true;
                try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
              };

              const playPromise = audio.play();
              if (playPromise !== undefined) {
                await playPromise;
              }

              audio.addEventListener('ended', signalStop, { once: true });
              audio.addEventListener('error', signalStop, { once: true });
              audio.addEventListener('pause', signalStop, { once: true });
              audio.addEventListener('stalled', signalStop, { once: true });
              audio.addEventListener('abort', signalStop, { once: true });
              setTimeout(signalStop, playbackSignalTimeoutMs);
              return audio;
            } catch (err) {
              console.error('VMO TTS: Playback error:', err.name, err.message, err);
              try { window.VMOAudioCoordinator && window.VMOAudioCoordinator.onTTSStop(); } catch(_) {}
              throw err;
            } finally {
              clearTimeout(timeoutId);
            }
          } catch (err) {
            clearTimeout(timeoutId);
            lastErr = err;
            const aborted = err && (err.name === 'AbortError');
            const status = err && err.status;
            const retryableStatus = status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
            if (!aborted && !err.isInvalidAudio && (retryableStatus || !status)) {
              console.warn('VMO TTS: REST attempt failed, retrying…', { attempt, status, message: err.message });
              await sleep(250 * attempt);
              continue;
            }
            throw err;
          }
        }

        throw lastErr || new Error('TTS failed');
      } catch (error) {
        // Config/API errors should NOT retry with different formats - immediately throw
        if (error.isConfigError || error.isApiKeyError) {
          console.warn('VMO TTS: Config error detected, skipping format retries');
          throw error;
        }
        
        // If the origin/master proxy is returning Cloudflare/gateway errors,
        // changing mp3 → opus → wav will not help; it only hammers the brand-new
        // QuickLaunch server and delays recovery. Bubble the transient error so
        // the voice panel can retry the same welcome quietly after the origin warms.
        const transientStatus = parseInt((error && error.status) || 0, 10);
        const transientMessage = String((error && error.message) || '').toLowerCase();
        if ([502, 503, 504, 521, 522, 523, 524].indexOf(transientStatus) !== -1 || transientMessage.indexOf('gateway') !== -1 || transientMessage.indexOf('cloudflare') !== -1 || transientMessage.indexOf('timed out') !== -1) {
          throw error;
        }

        // On actual audio-container/format issues, try fallback formats.
        if (tried.size < fallbackOrder.length) {
          const next = fallbackOrder.find(f => !tried.has(f));
          if (next) {
            console.warn('VMO TTS: Retrying with fallback format', { from: fmt, to: next });
            return attemptSpeak(next, tried);
          }
        }
        throw error;
      }
    }

    try {
      return await attemptSpeak(format);
    } catch (error) {
      console.error('VMO TTS Error:', error);
      throw error;
    }
  }

  // Expose globally
  window.vmoSpeak = vmoSpeak;

  /**
   * Auto-play TTS for elements with data-vmo-tts attribute
   * 
   * Usage:
   * <button data-vmo-tts="Hello world!" data-vmo-voice="alloy">Speak</button>
   * <a href="#" data-vmo-tts="Custom message">Click to hear</a>
   */
  document.addEventListener('click', async (e) => {
    const el = e.target.closest('[data-vmo-tts]');
    if (!el) return;

    e.preventDefault();

    const text = el.getAttribute('data-vmo-tts') || el.textContent.trim();
    const voice = el.getAttribute('data-vmo-voice') || 'ash';
    const format = el.getAttribute('data-vmo-format') || 'mp3';
    const speed = parseFloat(el.getAttribute('data-vmo-speed') || '1.0');

    if (!text) {
      console.warn('VMO TTS: No text found for element', el);
      return;
    }

    try {
      // Show loading state if element has a class for it
      el.classList.add('vmo-tts-loading');
      
      await vmoSpeak(text, voice, format, speed);
      
      // Remove loading state
      el.classList.remove('vmo-tts-loading');
      el.classList.add('vmo-tts-success');
      
      setTimeout(() => el.classList.remove('vmo-tts-success'), 2000);

    } catch (error) {
      el.classList.remove('vmo-tts-loading');
      el.classList.add('vmo-tts-error');
      setTimeout(() => el.classList.remove('vmo-tts-error'), 2000);
    }
  });

  /**
   * Get available voices
   */
  async function vmoGetVoices() {
    try {
      // Use admin-ajax.php for reliability (REST API permalinks can be flaky)
      const ajaxUrl = (window.vmo_ajax?.ajaxurl || '/wp-admin/admin-ajax.php');
      const url = ajaxUrl + '?action=vmo_tts_voices';
      
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error('Failed to fetch voices');
      }

      const data = await response.json();
      return data.voices || [];

    } catch (error) {
      console.error('VMO TTS: Error fetching voices:', error);
      // Fallback to static list if fetch fails
      return [
        { id: 'ash', name: 'Ash', description: 'Modern, confident tone' },
        { id: 'alloy', name: 'Alloy', description: 'Neutral and balanced' },
        { id: 'echo', name: 'Echo', description: 'Warm and engaging' },
        { id: 'fable', name: 'Fable', description: 'Expressive and dynamic' },
        { id: 'onyx', name: 'Onyx', description: 'Deep and authoritative' },
        { id: 'nova', name: 'Nova', description: 'Bright and friendly' },
        { id: 'shimmer', name: 'Shimmer', description: 'Clear and articulate' }
      ];
    }
  }

  // Expose voice fetcher globally
  window.vmoGetVoices = vmoGetVoices;

  console.log('✅ VMO TTS HD initialized');

})();
