import { io } from 'socket.io-client';

let socket = null;
let limited = false;
let limitPayload = null;
const limitListeners = new Set();
const sessionOkListeners = new Set();
let unloadBound = false;
let connectTimer = null;
let reconnectSeq = 0;

function notifyLimit(payload) {
  limited = true;
  limitPayload = payload || { max: 2 };
  for (const fn of limitListeners) {
    try {
      fn(limitPayload);
    } catch {
      /* ignore */
    }
  }
}

function notifyOk() {
  limited = false;
  limitPayload = null;
  for (const fn of limitListeners) {
    try {
      fn(null);
    } catch {
      /* ignore */
    }
  }
}

function notifySessionOk(payload) {
  for (const fn of sessionOkListeners) {
    try {
      fn(payload);
    } catch {
      /* ignore */
    }
  }
}

function clearConnectTimer() {
  if (connectTimer != null) {
    clearTimeout(connectTimer);
    connectTimer = null;
  }
}

function ensureConnected() {
  if (!socket || limited) return;
  socket.io.reconnection(true);
  if (!socket.connected) {
    clearConnectTimer();
    connectTimer = setTimeout(() => {
      connectTimer = null;
      if (!socket || limited || socket.connected) return;
      socket.connect();
    }, 50);
  }
}

function bindUnloadOnce() {
  if (unloadBound || typeof window === 'undefined') return;
  unloadBound = true;
  const release = () => {
    clearConnectTimer();
    if (socket?.connected) {
      // Manual disconnect frees the presence slot; pageshow/visibility will reconnect.
      socket.disconnect();
    }
  };
  // pagehide covers tab close, bfcache, and mobile backgrounding better than unload
  window.addEventListener('pagehide', release);
  window.addEventListener('pageshow', () => {
    ensureConnected();
  });
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') ensureConnected();
  });
}

export function getSocket() {
  if (!socket) {
    // Connect only after auth is known (see refreshSocketSession) to avoid
    // anonymous+auth double handshakes that stack slots on rapid refresh.
    socket = io({
      path: '/socket.io',
      withCredentials: true,
      autoConnect: false,
      reconnection: true,
      reconnectionDelay: 500,
      reconnectionAttempts: Infinity,
    });

    bindUnloadOnce();

    socket.on('session:limit', (payload) => {
      socket.io.reconnection(false);
      notifyLimit(payload);
    });

    socket.on('session:replaced', () => {
      // Another tab/refresh took our slot — stay quiet, allow a later reconnect
      // without showing the hard limit overlay.
      socket.io.reconnection(false);
    });

    socket.on('session:ok', (payload) => {
      if (!socket.io.reconnection()) {
        socket.io.reconnection(true);
      }
      notifyOk();
      notifySessionOk(payload);
    });
  }
  return socket;
}

/**
 * Force a new Socket.IO handshake so express-session reloads the auth cookie.
 * Needed after login/register/logout — otherwise an anonymous socket stays "offline".
 * Serialized so spam refresh cannot open many overlapping sockets from this tab.
 */
export function refreshSocketSession() {
  const s = getSocket();
  if (limited) return s;

  const seq = ++reconnectSeq;
  clearConnectTimer();
  s.io.reconnection(true);

  const connectNow = () => {
    if (seq !== reconnectSeq || limited) return;
    if (!s.connected) s.connect();
  };

  if (s.connected) {
    s.once('disconnect', () => {
      if (seq !== reconnectSeq) return;
      clearConnectTimer();
      connectTimer = setTimeout(connectNow, 40);
    });
    s.disconnect();
  } else {
    clearConnectTimer();
    connectTimer = setTimeout(connectNow, 0);
  }
  return s;
}

export function isSessionLimited() {
  return limited;
}

export function getSessionLimitPayload() {
  return limitPayload;
}

/** Subscribe to limit changes. Callback receives payload or null when cleared. */
export function onSessionLimitChange(fn) {
  limitListeners.add(fn);
  if (limited) fn(limitPayload);
  return () => limitListeners.delete(fn);
}

/** Fired when the server confirms an authenticated presence session. */
export function onSocketSessionOk(fn) {
  sessionOkListeners.add(fn);
  return () => sessionOkListeners.delete(fn);
}

/** Allow reconnect after the user closes other tabs and reloads. */
export function clearSessionLimit() {
  notifyOk();
  if (socket) {
    socket.io.reconnection(true);
    if (!socket.connected) socket.connect();
  }
}
