/* KARVAN client — mensajes temporales + WebRTC (data-channel).
 * Capa fiable: relay por servidor (RAM, autodestrucción) con polling.
 * Capa turbo: data-channel WebRTC P2P (instantáneo) con "perfect negotiation".
 * Todo degrada: si no hay WebRTC o no conecta, el chat sigue por el relay.
 * Sin cámara/mic (solo data-channel) => no necesita permisos del wrapper. */
(function () {
  "use strict";
  var root = document.querySelector(".karvan-room");
  if (!root) return;
  var roomId = root.getAttribute("data-room");
  var selfId = root.getAttribute("data-self") || "";
  var peerId = "p" + Math.random().toString(16).slice(2, 12);
  var fromLabel = selfId || peerId;

  var listEl = document.getElementById("karvan-msgs");
  var formEl = document.getElementById("karvan-form");
  var textEl = document.getElementById("karvan-text");
  var liveTxt = document.getElementById("karvan-live-txt");
  var liveWrap = document.getElementById("karvan-live");

  var seen = Object.create(null);
  var lastSeq = 0;
  var lastSig = 0;
  var peers = Object.create(null);

  // --- Fase 1: videollamada (media sobre el MISMO WebRTC del chat) ---
  var callRoot = document.getElementById("karvan-call");
  var localVid = document.getElementById("karvan-local");
  var remoteEl = document.getElementById("karvan-remote");
  var statusEl = document.getElementById("kc-status");
  var btnStart = document.getElementById("kc-start");
  var btnMic = document.getElementById("kc-mic");
  var btnCam = document.getElementById("kc-cam");
  var btnStop = document.getElementById("kc-stop");
  var localStream = (window.MediaStream ? new MediaStream() : null);
  if (localVid && localStream) localVid.srcObject = localStream;
  var micActive = false, camActive = false;

  function esc(s) { return String(s == null ? "" : s); }
  function shortId(id) { id = esc(id || "?"); return id.charAt(0) === "@" ? id.slice(1, 7) : id.slice(0, 6); }

  function showMsg(m, live) {
    var key = m.mid ? "m" + m.mid : "s" + (m.seq || Math.random());
    if (seen[key]) return;
    seen[key] = 1;
    var li = document.createElement("li");
    li.className = "karvan-msg" + (m.from === fromLabel ? " karvan-msg-self" : "") + (live ? " karvan-msg-live" : "");
    var f = document.createElement("span"); f.className = "km-from"; f.textContent = shortId(m.from);
    var t = document.createElement("span"); t.className = "km-text"; t.textContent = esc(m.text);
    li.appendChild(f); li.appendChild(t);
    listEl.appendChild(li);
    listEl.scrollTop = listEl.scrollHeight;
  }

  // --- relay por servidor (siempre activo) ---
  function pollMsgs() {
    fetch("/karvan/" + roomId + "/msgs?since=" + lastSeq, { headers: { Accept: "application/json" } })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (d) {
        if (!d || !d.messages) return;
        d.messages.forEach(function (m) { if (m.seq > lastSeq) lastSeq = m.seq; showMsg(m, false); });
      })
      .catch(function () {});
  }

  function sendMessage(text) {
    if (!text) return;
    var mid = peerId + "-" + Date.now().toString(36) + Math.random().toString(16).slice(2, 6);
    var m = { mid: mid, from: fromLabel, text: text };
    showMsg(m, false);                 // optimista
    broadcastDC({ kind: "chat", mid: mid, from: fromLabel, text: text }); // instantáneo a los pares
    fetch("/karvan/" + roomId + "/msg", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ from: fromLabel, text: text, mid: mid })
    }).catch(function () {});
  }

  if (formEl) {
    formEl.addEventListener("submit", function (e) {
      e.preventDefault();
      var v = (textEl.value || "").trim();
      if (!v) return;
      textEl.value = "";
      sendMessage(v);
    });
  }

  // --- señalización (mailbox RAM del servidor) ---
  function sendSig(to, payload) {
    fetch("/karvan/" + roomId + "/signal", {
      method: "POST", headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ from: peerId, to: to || "", payload: payload })
    }).catch(function () {});
  }
  function announce() { sendSig("", { kind: "hello" }); }

  function pollSignals() {
    fetch("/karvan/" + roomId + "/signal?for=" + peerId + "&since=" + lastSig, { headers: { Accept: "application/json" } })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (d) {
        if (!d) return;
        (d.signals || []).forEach(function (s) {
          if (s.seq > lastSig) lastSig = s.seq;
          onSignal(s.from, s.payload);
        });
        (d.members || []).forEach(function (mid2) { if (mid2 && mid2 !== peerId) ensurePeer(mid2); });
      })
      .catch(function () {});
  }

  // --- WebRTC (data-channel, perfect negotiation) ---
  var RTC = window.RTCPeerConnection || window.webkitRTCPeerConnection;
  // Sin servidores ICE por defecto: solo candidatos host (LAN y NAT amable).
  // Un STUN de terceros aprende la IP publica y el momento de cada llamada, asi que
  // no se pone ninguno de fabrica. Si el nodo ofrece TURN propio, /karvan/ice lo da.
  var rtcConfig = { iceServers: [] };

  function setLive(on) {
    if (!liveTxt) return;
    liveTxt.textContent = on ? "🔒 P2P (WebRTC)" : "server relay";
    if (liveWrap) liveWrap.className = "karvan-live" + (on ? " on" : "");
  }

  function broadcastDC(obj) {
    var s = JSON.stringify(obj);
    for (var k in peers) {
      var dc = peers[k] && peers[k].dc;
      if (dc && dc.readyState === "open") { try { dc.send(s); } catch (e) {} }
    }
  }

  function attachDC(peer, dc) {
    peer.dc = dc;
    dc.onopen = function () { setLive(true); };
    dc.onmessage = function (e) {
      try { var m = JSON.parse(e.data); if (m && m.kind === "chat") showMsg(m, true); } catch (x) {}
    };
    dc.onclose = function () {
      var any = false;
      for (var k in peers) if (peers[k].dc && peers[k].dc.readyState === "open") any = true;
      if (!any) setLive(false);
    };
  }

  function ensurePeer(remoteId) {
    if (peers[remoteId] || !RTC) return peers[remoteId];
    var polite = peerId > remoteId;              // el id mayor es "polite"
    var pc = new RTC(rtcConfig);
    var peer = { pc: pc, dc: null, polite: polite, makingOffer: false, ignoreOffer: false, vid: null, remoteStream: null };
    peers[remoteId] = peer;

    if (!polite) { attachDC(peer, pc.createDataChannel("chat")); } // el impolite inicia el canal
    pc.ondatachannel = function (e) { attachDC(peer, e.channel); };
    // media: publicar los tracks locales que ya tengamos (si la llamada está activa) + recibir los del par
    if (localStream) { localStream.getTracks().forEach(function (t) { try { pc.addTrack(t, localStream); } catch (e) {} }); }
    pc.ontrack = function (e) { attachRemote(peer, remoteId, e); };
    pc.onicecandidate = function (e) { if (e.candidate) sendSig(remoteId, { kind: "ice", candidate: e.candidate }); };
    pc.onnegotiationneeded = function () {
      peer.makingOffer = true;
      Promise.resolve()
        .then(function () { return pc.setLocalDescription(); })
        .then(function () { sendSig(remoteId, { kind: "desc", desc: pc.localDescription }); })
        .catch(function () {})
        .then(function () { peer.makingOffer = false; });
    };
    pc.onconnectionstatechange = function () {
      var st = pc.connectionState;
      if (st === "failed" || st === "closed") { try { pc.close(); } catch (e) {} removeRemoteVideo(peer); delete peers[remoteId]; }
    };
    return peer;
  }

  // --- media / videollamada (Fase 1) ---
  function setCallActive(on) { if (callRoot) callRoot.className = "karvan-call" + (on ? " active" : ""); }
  function setStatus(msg) { if (statusEl) statusEl.textContent = msg || ""; }
  function statusText() { var p = []; if (micActive) p.push("mic ON"); if (camActive) p.push("cam ON"); return p.length ? p.join(" · ") : ""; }
  function updateBtns() {
    if (btnMic) btnMic.className = "kc-btn" + (micActive ? " on" : "");
    if (btnCam) btnCam.className = "kc-btn" + (camActive ? " on" : "");
  }

  function attachRemote(peer, remoteId, e) {
    if (!remoteEl) return;
    if (!peer.vid) {
      peer.vid = document.createElement("video");
      peer.vid.autoplay = true; peer.vid.playsInline = true; peer.vid.className = "kc-video kc-remote-vid";
      peer.vid.setAttribute("data-peer", remoteId);
      remoteEl.appendChild(peer.vid);
    }
    if (e.streams && e.streams[0]) peer.vid.srcObject = e.streams[0];
    else { if (!peer.remoteStream) peer.remoteStream = new MediaStream(); peer.remoteStream.addTrack(e.track); peer.vid.srcObject = peer.remoteStream; }
    setCallActive(true);
  }
  function removeRemoteVideo(peer) {
    if (peer && peer.vid && peer.vid.parentNode) { try { peer.vid.parentNode.removeChild(peer.vid); } catch (_) {} peer.vid = null; }
  }

  function addLocalTrack(track) {
    if (!localStream) return;
    localStream.addTrack(track);
    for (var k in peers) {
      var pc = peers[k].pc, senders = pc.getSenders(), existing = null;
      for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === track.kind) { existing = senders[s]; break; }
      if (existing) { try { existing.replaceTrack(track); } catch (_) {} }   // swap (cambio de cámara) — sin renegociar
      else { try { pc.addTrack(track, localStream); } catch (_) {} }         // kind nuevo — dispara onnegotiationneeded
    }
  }
  function removeLocalKind(kind) {
    if (!localStream) return;
    localStream.getTracks().filter(function (t) { return t.kind === kind; }).forEach(function (t) { t.stop(); localStream.removeTrack(t); });
    for (var k in peers) {
      var senders = peers[k].pc.getSenders();
      for (var s = 0; s < senders.length; s++) if (senders[s].track && senders[s].track.kind === kind) { try { senders[s].replaceTrack(null); } catch (_) {} }
    }
  }
  function onMediaErr(kind, x) {
    var name = (x && x.name) || "", msg = (x && x.message) || String(x);
    if (name === "NotAllowedError" || /denied|permission/i.test(msg)) setStatus("⛔ Cámara/micro denegados (en el móvil, pendiente del wrapper). El chat sigue.");
    else if (name === "NotFoundError") setStatus("⚠ No hay " + (kind === "audio" ? "micrófono" : "cámara") + ".");
    else if (name === "NotReadableError") setStatus("⚠ Dispositivo ocupado.");
    else setStatus("⚠ " + (name || "error") + ": " + msg);
  }
  function getMedia(kind) {
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
    var c = (kind === "audio") ? { audio: true, video: false } : { audio: false, video: true };
    setStatus((kind === "audio" ? "🎤" : "🎥") + " pidiendo permiso…");
    navigator.mediaDevices.getUserMedia(c).then(function (s) {
      s.getTracks().forEach(addLocalTrack);
      if (kind === "audio") micActive = true; else camActive = true;
      setCallActive(true); updateBtns(); setStatus(statusText());
    }).catch(function (x) { onMediaErr(kind, x); });
  }
  function startCall() {
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setStatus("⚠ Cámara/micro no disponibles aquí."); return; }
    setStatus("📞 pidiendo cámara y micro…");
    navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then(function (s) {
      s.getTracks().forEach(addLocalTrack);
      micActive = true; camActive = true; setCallActive(true); updateBtns(); setStatus(statusText());
    }).catch(function (x) {
      if (x && x.name === "NotFoundError") { getMedia("audio"); return; } // sin cámara → intenta solo audio
      onMediaErr("call", x);
    });
  }
  function hangUp() {
    if (localStream) localStream.getTracks().forEach(function (t) { t.stop(); localStream.removeTrack(t); });
    for (var k in peers) { var sn = peers[k].pc.getSenders(); for (var s = 0; s < sn.length; s++) { try { sn[s].replaceTrack(null); } catch (_) {} } }
    micActive = false; camActive = false; setCallActive(false); updateBtns(); setStatus("");
  }
  if (btnStart) btnStart.addEventListener("click", startCall);
  if (btnMic) btnMic.addEventListener("click", function () { if (micActive) { removeLocalKind("audio"); micActive = false; updateBtns(); setStatus(statusText()); } else getMedia("audio"); });
  if (btnCam) btnCam.addEventListener("click", function () { if (camActive) { removeLocalKind("video"); camActive = false; updateBtns(); setStatus(statusText()); } else getMedia("video"); });
  if (btnStop) btnStop.addEventListener("click", hangUp);

  function onSignal(fromRemote, payload) {
    if (!RTC || !payload) return;
    var peer = ensurePeer(fromRemote);
    if (!peer) return;
    var pc = peer.pc;
    if (payload.kind === "desc") {
      var desc = payload.desc;
      if (!desc) return;                 // señal malformada (sin desc) — no reventar el lote de sondeo
      var collision = desc.type === "offer" && (peer.makingOffer || pc.signalingState !== "stable");
      peer.ignoreOffer = !peer.polite && collision;
      if (peer.ignoreOffer) return;
      Promise.resolve()
        .then(function () { return pc.setRemoteDescription(desc); })
        .then(function () {
          if (desc.type === "offer") {
            return pc.setLocalDescription().then(function () {
              sendSig(fromRemote, { kind: "desc", desc: pc.localDescription });
            });
          }
        })
        .catch(function () {});
    } else if (payload.kind === "ice") {
      Promise.resolve().then(function () { return pc.addIceCandidate(payload.candidate); }).catch(function () {});
    }
  }

  // --- arranque ---
  showMsgInitialSeq();
  function showMsgInitialSeq() {
    // registrar los mensajes ya renderizados por el servidor para no duplicarlos
    var items = listEl ? listEl.querySelectorAll(".karvan-msg") : [];
    for (var i = 0; i < items.length; i++) {
      var seq = parseInt(items[i].getAttribute("data-seq"), 10) || 0;
      var mid = items[i].getAttribute("data-mid") || "";
      if (seq > lastSeq) lastSeq = seq;
      seen[mid ? "m" + mid : "s" + seq] = 1;
    }
  }

  pollMsgs();
  setInterval(pollMsgs, 2000);
  if (RTC) {
    setLive(false);
    // La configuracion ICE tiene que estar resuelta antes de la primera
    // RTCPeerConnection: no se puede cambiar de forma fiable una ya creada.
    var iceReady = fetch("/karvan/ice", { headers: { accept: "application/json" } })
      .then(function (r) { return r.ok ? r.json() : null; })
      .then(function (d) {
        if (d && Array.isArray(d.iceServers) && d.iceServers.length) {
          rtcConfig = { iceServers: d.iceServers };
          if (d.relayOnly) rtcConfig.iceTransportPolicy = "relay";
        }
      })
      .catch(function () {});
    var iceTimeout = new Promise(function (res) { setTimeout(res, 2000); });
    Promise.race([iceReady, iceTimeout]).then(function () {
      announce();
      setInterval(announce, 8000);
      pollSignals();
      setInterval(pollSignals, 2000);
    });
  }
})();
