// public/app.js
// =================== CONST/API ===================
const API_CHUNK  = "../api/chunk.php";
const API_FEED   = "../api/feed.php";
const API_VOICES = "../api/eleven_voices.php";

// PTT
const SLICE_MS          = 3000;  // taille des slices en mode verrouillé
const HOLD_DELAY_MS     = 90;   // délai avant de lancer le hold (évite faux positifs)
const TAP_MAX_DURATION  = 160;   // si ≤ 160ms → tap
const DTAP_WINDOW_MS    = 300;   // double-tap si 2 taps dans 300ms

// Détermine si cette page est en mode "hôte" (créateur/orateur) : ajouter ?host=1 à l'URL
const IS_HOST = new URL(location.href).searchParams.get("host") === "1";

// =================== ÉTAT GLOBAL ===================
let stream;
let sessionId, clientId, channelId;
let seq = 0, sending = false;

// Feed (écoute)
let isJoined = false, isListening = false;
let pollTimer = null, lastSince = 0;
const playQueue = []; let isPlaying = false;

// PTT
let isHolding = false, isLatched = false;
let holdTimer = null, pressStartAt = 0, lastTapAt = 0;
let holdRec = null; // {mime,chunks[],recorder}
let activePointerId = null;

// Audio unlock
let audioCtx = null, audioReady = false;

// =================== UI BINDINGS ===================
const startBtn   = document.getElementById("startBtn");
const stopBtn    = document.getElementById("stopBtn");
const talkBtn    = document.getElementById("talkBtn");
const talkImg    = document.getElementById("talkImg");
const feedEl     = document.getElementById("feed");
const sourceLang = document.getElementById("sourceLang");
const targetLang = document.getElementById("targetLang");
const channelInp = document.getElementById("channel");
const ttsPlayer  = document.getElementById("ttsPlayer");
const audioGate  = document.getElementById("audioGate");
const enableAudioBtn = document.getElementById("enableAudioBtn");

// TTS provider & voices (UI peut ne pas exister si HTML pas à jour : on tolère)
const ttsProviderSel = document.getElementById("ttsProvider");
const elevenRow      = document.getElementById("elevenRow");
const elevenVoiceSel = document.getElementById("elevenVoiceSel");
const reloadVoicesBtn= document.getElementById("reloadVoicesBtn");
const previewVoiceBtn= document.getElementById("previewVoiceBtn");
const ttsVoiceRow    = document.getElementById("ttsVoiceRow");
const ttsVoiceInput  = document.getElementById("ttsVoiceInput");

// Map voiceId -> previewUrl
const elevenPreviewMap = new Map();

// =================== HELPERS ===================
function log(line){ feedEl.innerHTML += line + "\n"; feedEl.scrollTop = feedEl.scrollHeight; }
function ensureClientId(){
  try {
    const k='niva.clientId';
    let id = localStorage.getItem(k);
    if(!id){ id = crypto?.randomUUID?.() || String(Date.now()); localStorage.setItem(k,id); }
    clientId = id.replace(/[^a-z0-9-]/gi,'').toLowerCase();
  } catch {
    clientId = (crypto?.randomUUID?.() || String(Date.now())).replace(/[^a-z0-9-]/gi,'').toLowerCase();
  }
}
function pickMime(){
  const C = ['audio/ogg;codecs=opus','audio/webm;codecs=opus','audio/webm'];
  return C.find(m => window.MediaRecorder && MediaRecorder.isTypeSupported(m)) || '';
}
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }

// =================== AUDIO AUTOPLAY UNLOCK ===================
async function unlockAudio() {
  if (audioReady) return true;

  // A. WebAudio (sous geste utilisateur)
  try {
    audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
    const o = audioCtx.createOscillator();
    const g = audioCtx.createGain();
    g.gain.value = 0; // silencieux
    o.connect(g); g.connect(audioCtx.destination);
    o.start(); o.stop(audioCtx.currentTime + 0.02);
    await audioCtx.resume();
    audioReady = true;
    audioGate.style.display = "none";
    return true;
  } catch (e) {
    // continue vers B
  }

  // B. Fallback: essayer une lecture très courte (débloque certains Safari)
  try {
    // (data URI silencieuse très courte ; acceptable pour un unlock)
    const silent = "data:audio/mp3;base64,//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    ttsPlayer.muted = true;
    ttsPlayer.src = silent;
    await ttsPlayer.play();
    ttsPlayer.pause(); ttsPlayer.currentTime = 0;
    ttsPlayer.muted = false;
    audioReady = true;
    audioGate.style.display = "none";
    return true;
  } catch (e2) {
    audioGate.style.display = "flex";
    return false;
  }
}
// Déclencheurs iOS/Safari (captures de gestes)
['click','touchstart'].forEach(evt => {
  window.addEventListener(evt, () => { if (!audioReady) unlockAudio(); }, { once:true, passive:true, capture:true });
});
if (enableAudioBtn) {
  enableAudioBtn.addEventListener("click", () => { unlockAudio(); });
  enableAudioBtn.addEventListener("touchend", (e)=>{ e.preventDefault(); unlockAudio(); }, { passive:false });
}

// =================== JOIN / FEED ===================
async function start(){
  const ch = parseFloat(channelInp.value);
  if(isNaN(ch) || ch < 0 || ch > 111.11){ log("ERREUR : channel invalide (0–111.11)"); return; }
  channelId = ch.toFixed(2);
  ensureClientId();
  sessionId = crypto?.randomUUID?.() || String(Date.now());
  seq = 0;

  await unlockAudio(); // débloque l'audio dès le join

  // on se cale sur "maintenant" (pas d'historique)
  isJoined = true; isListening = true;
  startPolling({ cursorNow:true });

  startBtn.disabled = true; stopBtn.disabled = false;
  talkBtn.setAttribute("aria-pressed","false");
  log(`>> Rejoint le channel ${channelId} ${IS_HOST ? "(hôte)" : "(auditeur)"}`);
}
function stop(){
  isJoined = false; isListening = false;
  if(pollTimer){ clearTimeout(pollTimer); pollTimer = null; }
  lastSince = 0;

  stopLatched(); stopHold(true);
  if(stream){ try{ stream.getTracks().forEach(t=>t.stop()); }catch{} stream = null; }

  talkBtn.classList.remove('holding','latched');
  startBtn.disabled = false; stopBtn.disabled = true;
  log(">> Quitté le channel");
}
function startPolling({cursorNow=false}={}){
  const poll = async ()=>{
    if(!isListening) return;
    try{
      let url;
      if(cursorNow){
        url = `${API_FEED}?channel=${encodeURIComponent(channelId)}&cursor=now&limit=0`;
        cursorNow = false;
      }else{
        url = `${API_FEED}?channel=${encodeURIComponent(channelId)}&since=${encodeURIComponent(lastSince)}&exclude_speaker_id=${encodeURIComponent(clientId)}&exclude_session_id=${encodeURIComponent(sessionId)}&viewer_is_host=${IS_HOST?1:0}&limit=50`;
      }
      const res = await fetch(url);
      const data = await res.json();
      if(data?.ok){
        if(typeof data.next_since === 'number') lastSince = data.next_since;
        if(Array.isArray(data.items) && data.items.length){
          for(const it of data.items){
            // garde-fous client
            if(it.speaker_id && String(it.speaker_id).toLowerCase() === clientId) continue;
            if(it.session_id && String(it.session_id) === sessionId) continue;

            const prov = it.tts_provider ? ` • TTS:${it.tts_provider}${it.tts_voice?`/${it.tts_voice}`:''}` : '';
            log(`[feed] ${it.channel} • ${it.source_lang}▶${it.target_lang}${prov}\nTRD: ${it.text}\n`);
            if (it.tts_url) playQueue.push(it.tts_url);
          }
          if(!isPlaying) playNext();
        }
      }
    }catch(e){
      // silencieux
    }
    finally{ pollTimer = setTimeout(poll, 900); }
  };
  if(pollTimer) clearTimeout(pollTimer);
  pollTimer = setTimeout(poll, 50);
}

// =================== PLAYER TTS ===================
async function playNext(){
  if(!isListening || isPlaying) return;
  const next = playQueue.shift(); if(!next) return;

  if (!audioReady) {
    const ok = await unlockAudio();
    if (!ok) { playQueue.unshift(next); setTimeout(playNext, 800); return; }
  }

  isPlaying = true;
  ttsPlayer.src = next;
  try { await ttsPlayer.play(); }
  catch (err) {
    // NotAllowedError (autoplay) ou autre → affiche le bandeau
    audioGate.style.display = "flex";
    playQueue.unshift(next);
    isPlaying = false;
    return;
  }
  const onDone = ()=>{ ttsPlayer.removeEventListener('ended', onDone); ttsPlayer.removeEventListener('error', onErr); isPlaying=false; playNext(); };
  const onErr  = ()=>{ ttsPlayer.removeEventListener('ended', onDone); ttsPlayer.removeEventListener('error', onErr); isPlaying=false; playNext(); };
  ttsPlayer.addEventListener('ended', onDone, { once:true });
  ttsPlayer.addEventListener('error', onErr, { once:true });
}

// =================== MICRO / CAPTURE ===================
async function ensureMic(){
  if(stream) return stream;
  stream = await navigator.mediaDevices.getUserMedia({
    audio:{ channelCount:1, sampleRate:48000, echoCancellation:true, noiseSuppression:false, autoGainControl:false }
  });
  return stream;
}

// =================== HOLD-TO-TALK ===================
async function startHold(){
  if(!isJoined || isHolding || isLatched) return;
  await ensureMic();
  const mime = pickMime();
  holdRec = { mime, chunks:[], recorder:null };
  const rec = new MediaRecorder(stream, { mimeType:mime, audioBitsPerSecond:128000 });
  holdRec.recorder = rec; isHolding = true;
  talkBtn.classList.add('holding');
  talkBtn.setAttribute("aria-pressed","true");

  rec.ondataavailable = (e)=>{ if(e.data && e.data.size) holdRec.chunks.push(e.data); };
  rec.onstop = async ()=>{
    try{
      if(!holdRec) return;
      const blob = new Blob(holdRec.chunks, { type: holdRec.mime || 'audio/webm' });
      holdRec = null;
      await sendChunk(blob); // un seul chunk = tout le speech
    }finally{
      isHolding = false;
    }
  };
  rec.start();
}
function stopHold(force=false){
  if(!holdRec?.recorder) return;
  // feedback visuel immédiat
  talkBtn.classList.remove('holding');
  talkBtn.setAttribute("aria-pressed","false");
  try{
    if(holdRec.recorder.state !== 'inactive'){
      // stop direct pour réduire la latence perçue
      holdRec.recorder.stop();
    }
  }catch{}
}

// =================== TALK "LATCHED" (slices) ===================
async function startLatched(){
  if(!isJoined || isLatched) return;
  await ensureMic();
  isLatched = true;
  talkBtn.classList.add('latched');
  talkBtn.setAttribute("aria-pressed","true");

  const loop = ()=>{
    if(!isLatched) return;
    const mime = pickMime();
    const rec = new MediaRecorder(stream,{ mimeType:mime, audioBitsPerSecond:128000 });
    rec.ondataavailable = (e)=>{ if(e.data && e.data.size) sendChunk(e.data); };
    rec.onstop = ()=>{ if(isLatched) setTimeout(loop,0); };

    rec.start();
    setTimeout(()=>{
      try{ if(rec.state!=='inactive') rec.stop(); }catch{}
    }, SLICE_MS);
  };
  loop();
}
function stopLatched(){
  if(!isLatched) return;
  isLatched = false;
  talkBtn.classList.remove('latched');
  talkBtn.setAttribute("aria-pressed","false");
}

// =================== ENVOI D’UN CHUNK ===================
async function sendChunk(blob){
  while(sending){ await new Promise(r=>setTimeout(r,40)); }
  sending = true;
  try{
    const fd = new FormData();
    const ext = blob.type.includes('ogg') ? 'ogg' : 'webm';
    fd.append("audio", blob, `seq-${seq}.${ext}`);
    fd.append("session_id", sessionId);
    fd.append("speaker_id", clientId);
    fd.append("speaker_role", IS_HOST ? "host" : "listener");
    fd.append("channel", channelId);
    fd.append("seq", String(seq));
    fd.append("source_lang", sourceLang.value);
    fd.append("target_lang", targetLang.value);

    // Provider & voice depuis l'UI (si absents → backend utilisera TTS_DEFAULT_PROVIDER)
    const provider = ttsProviderSel ? ttsProviderSel.value : '';
    if (provider) fd.append("tts_provider", provider);
    if (provider === 'elevenlabs' && elevenVoiceSel) {
      fd.append("tts_voice", elevenVoiceSel.value || '');
    } else if (provider === 'openai' && ttsVoiceInput) {
      fd.append("tts_voice", (ttsVoiceInput.value || '').trim());
    } else {
      fd.append("tts_voice", '');
    }

    const res = await fetch(API_CHUNK, { method:"POST", body:fd });
    const raw = await res.text();
    let json; try{ json = JSON.parse(raw); }catch{ log(`ERREUR serveur (non-JSON): ${raw.slice(0,200)}...`); return; }
    const seqLabel = (json && json.seq !== null && typeof json.seq !== 'undefined') ? json.seq : '?';
    if(json.ok){
      const pv = json.tts_provider ? ` • TTS:${json.tts_provider}${json.tts_voice?`/${json.tts_voice}`:''}` : '';
      let warn = json.warn ? `\nNOTE: ${json.warn}` : '';
      log(`[${seqLabel}] ${json.source_lang} ▶ ${json.target_lang}${pv}\nSTT: ${json.text}\nTRD: ${json.translated}${warn}\n`);
      // La lecture audio se fait via le feed uniquement
    }else{
      log(`ERREUR [${seqLabel}] : ${json.error || "inconnue"}`);
    }
  }catch(e){
    log("ERREUR réseau : " + (e?.message || e));
  }finally{
    seq++; sending=false;
  }
}

// =================== ELEVENLABS VOICES ===================
async function loadElevenVoices(refresh=false) {
  if (!elevenVoiceSel) return;
  try {
    elevenVoiceSel.innerHTML = `<option value="">Chargement…</option>`;
    const url = refresh ? `${API_VOICES}?refresh=1` : API_VOICES;
    const res = await fetch(url);
    const data = await res.json();
    if (!data?.ok || !Array.isArray(data.voices)) {
      elevenVoiceSel.innerHTML = `<option value="">Erreur chargement (voir console)</option>`;
      console.warn('eleven_voices error', data);
      return;
    }
    elevenPreviewMap.clear();
    const opts = [`<option value="">— Choisir une voix —</option>`];
    for (const v of data.voices) {
      const labelBits = [v.name];
      if (v.labels) {
        const meta = [];
        if (v.labels.accent) meta.push(v.labels.accent);
        if (v.labels.gender) meta.push(v.labels.gender);
        if (v.labels.age)    meta.push(v.labels.age);
        if (meta.length) labelBits.push(`(${meta.join(' · ')})`);
      }
      const label = labelBits.join(' ');
      const preview = v.preview_url || '';
      elevenPreviewMap.set(v.id, preview);
      opts.push(`<option value="${escapeHtml(v.id)}" data-preview="${escapeHtml(preview)}">${escapeHtml(label)}</option>`);
    }
    elevenVoiceSel.innerHTML = opts.join('');
  } catch (e) {
    console.error(e);
    elevenVoiceSel.innerHTML = `<option value="">Erreur réseau</option>`;
  }
}
if (reloadVoicesBtn) reloadVoicesBtn.addEventListener('click', ()=> loadElevenVoices(true));
if (previewVoiceBtn) previewVoiceBtn.addEventListener('click', async ()=>{
  if (!elevenVoiceSel) return;
  const id = elevenVoiceSel.value;
  if (!id) return;
  const url = elevenPreviewMap.get(id);
  if (!url) return;
  await unlockAudio();
  try { ttsPlayer.src = url; await ttsPlayer.play(); } catch (e) { /* ignore */ }
});

// =================== UI Provider <-> Inputs ===================
function syncTtsUi() {
  if (!ttsProviderSel) return;
  const p = ttsProviderSel.value;
  if (elevenRow)   elevenRow.style.display = (p === 'elevenlabs') ? 'flex' : 'none';
  if (ttsVoiceRow) ttsVoiceRow.style.display = (p === 'openai')    ? 'flex' : 'none';
  if (p === 'elevenlabs' && elevenVoiceSel && (!elevenVoiceSel.options.length || elevenVoiceSel.value === '')) {
    loadElevenVoices(false);
  }
}
if (ttsProviderSel) ttsProviderSel.addEventListener('change', syncTtsUi);
document.addEventListener('DOMContentLoaded', ()=>{
  // valeur par défaut canal si vide
  if (channelInp && !channelInp.value) channelInp.value = '1.01';
  syncTtsUi();
});

// =================== BOUTON IMAGE (PTT) ===================
function onPointerDown(ev){
  if(!isJoined) return;
  ev.preventDefault();
  unlockAudio(); // débloque si besoin dès l'appui
  activePointerId = ev.pointerId;
  try{ talkBtn.setPointerCapture(ev.pointerId); }catch{}
  pressStartAt = Date.now();
  holdTimer = setTimeout(()=>{ startHold(); }, HOLD_DELAY_MS);
}
function onPointerUp(ev){
  if(!isJoined) return;
  ev.preventDefault();
  if(activePointerId !== null){ try{ talkBtn.releasePointerCapture(activePointerId); }catch{} activePointerId = null; }

  const now = Date.now();
  const pressDur = now - pressStartAt;
  if(holdTimer){ clearTimeout(holdTimer); holdTimer = null; }

  if(pressDur <= TAP_MAX_DURATION){
    if(now - lastTapAt <= DTAP_WINDOW_MS){
      if(isLatched) stopLatched(); else startLatched();
      lastTapAt = 0;
    }else{
      lastTapAt = now; // simple tap: rien
    }
  }else{
    if(!isLatched) stopHold();
  }
}
function onPointerCancel(){
  if(holdTimer){ clearTimeout(holdTimer); holdTimer = null; }
  if(!isLatched) stopHold(true);
  activePointerId = null;
}

// =================== LISTENERS ===================
if (startBtn) startBtn.addEventListener("click", start);
if (stopBtn)  stopBtn .addEventListener("click", stop);

if (talkBtn) {
  talkBtn.addEventListener("pointerdown", onPointerDown);
  talkBtn.addEventListener("pointerup", onPointerUp);
  talkBtn.addEventListener("pointercancel", onPointerCancel);
  talkBtn.addEventListener("lostpointercapture", onPointerCancel);
  talkBtn.addEventListener("pointerleave", (e)=>{ if(e.buttons===0) onPointerCancel(); });
}

// Sécurité à la fermeture onglet
window.addEventListener("visibilitychange", ()=>{ if(document.hidden){ if(!isLatched) stopHold(true); }});
window.addEventListener("beforeunload", ()=>{ try{ stop(); }catch{} });
