import { prisma } from './db.js';
import { settleVaultYield } from './vault.js';

export const MAX_SOCKETS_PER_USER = Math.max(
  1,
  Number(process.env.MAX_SOCKETS_PER_USER || 2)
);

/** @type {Map<number, { sockets: Set<string>, sessionStartedAt: number, lastVaultCredit?: number }>} */
const online = new Map();

export function isConnected(userId) {
  return online.has(Number(userId));
}

export function connectionCount(userId) {
  return online.get(Number(userId))?.sockets.size || 0;
}

export function onlineUserCount() {
  return online.size;
}

export function onlineUserIds() {
  return [...online.keys()];
}

export function totalSocketCount() {
  let n = 0;
  for (const entry of online.values()) {
    n += entry.sockets.size;
  }
  return n;
}

/**
 * Track an authenticated socket. Updates lastConnection on first socket for this user.
 * Caps concurrent sockets at MAX_SOCKETS_PER_USER. When full, the oldest socket is
 * replaced (covers refresh spam / ghost connections) instead of blocking the new tab.
 * Slot claim is synchronous so concurrent connects cannot exceed the max.
 * @param {import('socket.io').Server | null} [io]
 * @returns {Promise<{ ok: true, count: number, max: number, vaultCredited?: string, replaced?: string[] } | { ok: false, count: number, max: number }>}
 */
export async function trackConnect(userId, socketId, io = null) {
  const id = Number(userId);
  const max = MAX_SOCKETS_PER_USER;
  if (!id || !socketId) {
    return { ok: false, count: 0, max };
  }

  let entry = online.get(id);
  if (!entry) {
    entry = { sockets: new Set(), sessionStartedAt: Date.now() };
    online.set(id, entry);
  }

  // Drop ids that are no longer connected (HMR / crashed tabs leave ghosts otherwise)
  pruneDeadSockets(entry, io);

  if (entry.sockets.size === 0) {
    entry.sessionStartedAt = Date.now();
  }

  if (entry.sockets.has(socketId)) {
    return { ok: true, count: entry.sockets.size, max };
  }

  // At capacity: free the oldest slot(s) so a refresh never soft-locks the only open tab
  const replaced = [];
  while (entry.sockets.size >= max) {
    const oldest = entry.sockets.values().next().value;
    if (!oldest) break;
    entry.sockets.delete(oldest);
    replaced.push(oldest);
    const oldSock = io?.sockets?.sockets?.get(oldest);
    if (oldSock) {
      try {
        oldSock.emit('session:replaced', {
          reason: 'slot_taken',
          max,
        });
      } catch {
        /* ignore */
      }
      oldSock.disconnect(true);
    }
  }

  // Claim slot before any await (avoids race past the limit)
  entry.sockets.add(socketId);
  const isFirst = entry.sockets.size === 1 && replaced.length === 0;
  let vaultCredited = '0';

  if (isFirst) {
    try {
      await prisma.user.update({
        where: { id },
        data: { lastConnection: new Date() },
      });
    } catch (err) {
      console.error('presence lastConnection', err);
    }
    // Offline yield accrued while disconnected
    try {
      const settled = await settleVaultYield(id, 'offline');
      vaultCredited = settled.credited || '0';
    } catch (err) {
      console.error('presence vault offline settle', err);
    }
  }

  return { ok: true, count: entry.sockets.size, max, vaultCredited, replaced };
}

function pruneDeadSockets(entry, io) {
  if (!entry?.sockets?.size) return;
  if (!io?.sockets?.sockets) return;
  for (const sid of [...entry.sockets]) {
    if (!io.sockets.sockets.has(sid)) {
      entry.sockets.delete(sid);
    }
  }
}

/**
 * Remove a socket. When the last socket for a user drops, add session time to playTimeSeconds.
 */
export async function trackDisconnect(userId, socketId) {
  const id = Number(userId);
  if (!id || !socketId) return;

  const entry = online.get(id);
  if (!entry) return;

  entry.sockets.delete(socketId);
  if (entry.sockets.size > 0) return;

  online.delete(id);
  const elapsed = Math.max(0, Math.floor((Date.now() - entry.sessionStartedAt) / 1000));

  // Settle remaining online ticks before going offline
  try {
    await settleVaultYield(id, 'online');
  } catch (err) {
    console.error('presence vault online settle', err);
  }

  if (elapsed <= 0) return;

  try {
    await prisma.user.update({
      where: { id },
      data: { playTimeSeconds: { increment: elapsed } },
    });
  } catch (err) {
    console.error('presence playTime', err);
  }
}

/** Live playtime including the current open session (if any). */
export function livePlayTimeSeconds(user) {
  let stored = 0;
  try {
    // playTimeSeconds is Prisma BigInt — avoid Number() on the raw value path
    // for consistency; seconds stay well within safe integer range for decades.
    stored = Number(typeof user?.playTimeSeconds === 'bigint'
      ? user.playTimeSeconds
      : BigInt(user?.playTimeSeconds ?? 0));
  } catch {
    stored = 0;
  }
  if (!Number.isFinite(stored) || stored < 0) stored = 0;
  const entry = online.get(Number(user?.id));
  if (!entry) return stored;
  const live = Math.max(0, Math.floor((Date.now() - entry.sessionStartedAt) / 1000));
  return stored + live;
}

export function serializePresence(user) {
  return {
    isConnected: isConnected(user.id),
    playTimeSeconds: livePlayTimeSeconds(user),
    lastConnection: user.lastConnection || null,
    openWindows: connectionCount(user.id),
    maxWindows: MAX_SOCKETS_PER_USER,
  };
}

/** Settle online vault yield for all currently connected users. */
export async function settleOnlineVaultForConnected() {
  const ids = onlineUserIds();
  let total = 0n;
  for (const uid of ids) {
    try {
      const settled = await settleVaultYield(uid, 'online');
      total += BigInt(settled.credited || '0');
    } catch (err) {
      console.error('vault online tick', uid, err);
    }
  }
  return total.toString();
}
