// Chat presence + real-time delivery. A logged-in user opens a WebSocket and sends `chat-hello`;
// signaling.js registers the socket here. Messages are persisted over HTTP (routes.js) and pushed live to
// the recipient's sockets via pushToUser().
//
// MULTI-INSTANCE: local delivery (to sockets on THIS process) is unchanged. Every push is ALSO published
// via the swappable pubsub layer so, when >1 instance runs, a recipient connected to another instance
// still gets it. With the default in-memory pubsub (single instance) publish is a no-op, so this is
// behaviourally identical to before — no hot-path cost.
const { chatClients, meetingRooms } = require('./presence');
const pubsub = require('./pubsub');
let _repos = null; const repos = () => (_repos || (_repos = require('./repos'))); // lazy: avoid a require cycle

// Deliver an already-built or plain object to a user's sockets on THIS instance.
function deliverLocal(userId, obj) {
  const s = chatClients.get(userId);
  if (!s) return;
  const data = typeof obj === 'string' ? obj : JSON.stringify(obj);
  for (const ws of s) { if (ws.readyState === 1) { try { ws.send(data); } catch (_) {} } }
}

function register(userId, ws) {
  if (!chatClients.has(userId)) chatClients.set(userId, new Set());
  chatClients.get(userId).add(ws);
  ws._chatUserId = userId;
  repos().users.touchSeen(userId).catch(() => {}); // "last seen" (#2) — fire-and-forget UPDATE
}

function unregister(ws) {
  const id = ws && ws._chatUserId;
  if (!id) return;
  const set = chatClients.get(id);
  if (set) {
    set.delete(ws);
    // Their LAST socket went away → stamp when they were last here, so contacts can show "last seen …".
    if (!set.size) { chatClients.delete(id); repos().users.touchSeen(id).catch(() => {}); }
  }
}

function isOnline(userId) {
  const s = chatClients.get(userId);
  return !!(s && s.size);
}

function pushToUser(userId, obj) {
  deliverLocal(userId, obj);            // sockets on this instance
  pubsub.publish('u:' + userId, obj);   // other instances (no-op on the memory backend)
}

// --- Live presence -------------------------------------------------------------------------
// A user's effective status (online / in-a-call / away / …) changes with no HTTP round-trip: they connect
// or disconnect a socket, or join/leave a call. Without pushing that change, OTHER users only see it after
// a full page reload — impossible in the desktop/mobile apps. So whenever it changes we broadcast the
// user's fresh status to everyone else's sockets, and the client updates that contact's dot/subtitle.
function isInCall(userId) {
  for (const [, peers] of meetingRooms) { for (const [, p] of peers) { if (p.ws && p.ws._meetingUserId === userId) return true; } }
  return false;
}
async function effectiveStatus(userId) {
  if (isInCall(userId)) return 'incall';           // derived (overrides the stored status)
  try { const u = await repos().users.byId(userId); return (u && u.status) || 'active'; } catch (_) { return 'active'; }
}
// Send a presence payload to every local socket EXCEPT the subject's own.
function deliverPresenceLocal(subjectId, payload) {
  for (const [uid, set] of chatClients) {
    if (uid === subjectId) continue;
    for (const ws of set) { if (ws.readyState === 1) { try { ws.send(payload); } catch (_) {} } }
  }
}
async function broadcastPresence(userId) {
  if (!userId) return;
  // Carry last_seen too, so a contact going offline immediately reads "Last seen just now" instead of a
  // bare "Offline" until the next sidebar reload (#2).
  let lastSeen = null;
  try { const u = await repos().users.byId(userId); lastSeen = (u && u.last_seen) || null; } catch (_) {}
  const payload = JSON.stringify({ type: 'presence', userId, online: isOnline(userId), status: await effectiveStatus(userId), lastSeen });
  deliverPresenceLocal(userId, payload);                    // this instance
  pubsub.publish('presence', { userId, payload });          // other instances (no-op on memory)
}

// Cross-instance inbound: deliver events published by OTHER instances to our local sockets. On the memory
// backend these never fire; on redis they carry the fan-out. (Buffered until pubsub.init() connects.)
pubsub.subscribe('u:*', (channel, obj) => deliverLocal(channel.slice(2), obj));
pubsub.subscribe('presence', (channel, msg) => { if (msg && msg.payload) deliverPresenceLocal(msg.userId, msg.payload); });

module.exports = { register, unregister, isOnline, pushToUser, broadcastPresence };
