/**
 * Calls WebSocket Signaling Relay
 *
 * Provides /calls/ws WebSocket endpoint for peer-to-peer call signaling.
 * Independent of the companion's WebSocket (OLV). Works standalone.
 *
 * Room lifecycle:
 *   - Room created on first peer join
 *   - Destroyed when last peer leaves
 *   - Token validated at connect time only (not per-message)
 *
 * Message types relayed between room members:
 *   webrtc-signal, webrtc-mute, webrtc-video-state, webrtc-representation,
 *   webrtc-bandwidth-pref, peer-profile, avatar-state, avatar-interaction
 *
 * Broadcasts generated by server:
 *   group-update (on join/leave with full member list)
 */

import { WebSocketServer } from "ws";
import { validateCallRoomToken } from "./calls-page.js";

const MAX_PEERS = parseInt(process.env.CROW_CALLS_MAX_PEERS || "4", 10);
const RECONNECT_WINDOW_MS = 30_000; // 30s window for reconnection without triggering leave

// Room state: Map<roomCode, Map<uid, { ws, profile, joinedAt }>>
const _rooms = new Map();

// Recently disconnected peers: Map<`${roomCode}:${uid}`, { profile, disconnectedAt }>
const _recentlyLeft = new Map();

// Relayed message types (forwarded to all other room members)
const RELAY_TYPES = new Set([
  "webrtc-signal",
  "webrtc-mute",
  "webrtc-video-state",
  "webrtc-representation",
  "webrtc-bandwidth-pref",
  "peer-profile",
  "avatar-state",
  "avatar-interaction",
]);

/**
 * Broadcast a message to all peers in a room except the sender.
 */
function broadcastToRoom(roomCode, senderUid, message) {
  const room = _rooms.get(roomCode);
  if (!room) return;
  const raw = typeof message === "string" ? message : JSON.stringify(message);
  for (const [uid, peer] of room) {
    if (uid !== senderUid && peer.ws.readyState === 1) {
      peer.ws.send(raw);
    }
  }
}

/**
 * Broadcast group-update to all members (including the triggering peer).
 */
function broadcastGroupUpdate(roomCode) {
  const room = _rooms.get(roomCode);
  if (!room) return;
  const members = [];
  for (const [uid, peer] of room) {
    members.push({ uid, ...peer.profile });
  }
  const msg = JSON.stringify({ type: "group-update", members });
  for (const [, peer] of room) {
    if (peer.ws.readyState === 1) peer.ws.send(msg);
  }
}

/**
 * Handle a new peer joining a room.
 */
function handleJoin(roomCode, uid, ws, profile) {
  if (!_rooms.has(roomCode)) {
    _rooms.set(roomCode, new Map());
  }
  const room = _rooms.get(roomCode);

  // Check if this is a reconnection
  const recentKey = `${roomCode}:${uid}`;
  const recent = _recentlyLeft.get(recentKey);
  if (recent) {
    _recentlyLeft.delete(recentKey);
    // Use previous profile if no new one provided
    if (!profile.name && recent.profile.name) {
      profile = { ...recent.profile, ...profile };
    }
  }

  // Enforce max peers
  if (room.size >= MAX_PEERS && !room.has(uid)) {
    ws.send(JSON.stringify({ type: "error", error: "room_full", maxPeers: MAX_PEERS }));
    ws.close(4001, "Room full");
    return false;
  }

  // Close any existing connection for this uid (duplicate tab)
  const existing = room.get(uid);
  if (existing && existing.ws !== ws) {
    existing.ws.close(4002, "Replaced by new connection");
  }

  room.set(uid, { ws, profile, joinedAt: Date.now() });
  broadcastGroupUpdate(roomCode);
  return true;
}

/**
 * Handle a peer leaving a room.
 */
function handleLeave(roomCode, uid) {
  const room = _rooms.get(roomCode);
  if (!room) return;

  const peer = room.get(uid);
  if (!peer) return;

  // Store in recently-left for reconnection window
  _recentlyLeft.set(`${roomCode}:${uid}`, {
    profile: peer.profile,
    disconnectedAt: Date.now(),
  });

  room.delete(uid);

  if (room.size === 0) {
    _rooms.delete(roomCode);
  } else {
    broadcastGroupUpdate(roomCode);
  }
}

// Periodic cleanup of stale recently-left entries
setInterval(() => {
  const now = Date.now();
  for (const [key, entry] of _recentlyLeft) {
    if (now - entry.disconnectedAt > RECONNECT_WINDOW_MS) {
      _recentlyLeft.delete(key);
    }
  }
}, 15_000);

/**
 * Set up the calls WebSocket signaling relay.
 *
 * IMPORTANT: This upgrade handler MUST be registered BEFORE companion
 * and extension proxy handlers to get first crack at /calls/ws.
 * When /calls/ws matches, the socket is consumed (upgraded). When it
 * doesn't match, return lets subsequent handlers process it.
 *
 * @param {import('http').Server} server
 */
export default function setupCallsSignaling(server) {
  const wss = new WebSocketServer({ noServer: true });

  server.on("upgrade", (req, socket, head) => {
    const url = req.url || "";
    if (!url.startsWith("/calls/ws")) return; // Let other handlers process

    // Parse query params
    const params = new URL(url, "http://localhost").searchParams;
    const room = params.get("room");
    const token = params.get("token");
    const uid = params.get("uid");

    if (!room || !uid) {
      socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
      socket.destroy();
      return;
    }

    // Validate room token (only at connect time)
    if (token) {
      const result = validateCallRoomToken(room, token);
      if (!result) {
        socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
        socket.destroy();
        return;
      }
    } else {
      // No token provided — reject. Room tokens are always required.
      socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
      socket.destroy();
      return;
    }

    // Upgrade the connection (consumes the socket)
    wss.handleUpgrade(req, socket, head, (ws) => {
      const profile = {
        name: params.get("name") || "User",
        color: params.get("color") || null,
      };

      const joined = handleJoin(room, uid, ws, profile);
      if (!joined) return; // Room full, ws already closed

      // Heartbeat: detect dead connections
      ws._isAlive = true;
      ws.on("pong", () => { ws._isAlive = true; });

      ws.on("message", (raw) => {
        let msg;
        try {
          msg = JSON.parse(raw);
        } catch {
          return; // Ignore non-JSON messages
        }

        const type = msg.type;
        if (!type) return;

        // Peer profile update
        if (type === "peer-profile") {
          const roomData = _rooms.get(room);
          const peer = roomData?.get(uid);
          if (peer) {
            if (msg.name) peer.profile.name = msg.name;
            if (msg.color) peer.profile.color = msg.color;
          }
        }

        // Relay message to room members
        if (RELAY_TYPES.has(type)) {
          // Inject sender uid so recipients know who sent it
          msg.from = uid;

          // For webrtc-signal with a target, send only to that peer
          if (type === "webrtc-signal" && msg.to) {
            const roomData = _rooms.get(room);
            const target = roomData?.get(msg.to);
            if (target && target.ws.readyState === 1) {
              target.ws.send(JSON.stringify(msg));
            }
            return;
          }

          broadcastToRoom(room, uid, msg);
        }
      });

      ws.on("close", () => {
        handleLeave(room, uid);
      });

      ws.on("error", () => {
        handleLeave(room, uid);
      });
    });
  });

  // Heartbeat interval: detect and clean up dead connections
  const heartbeatInterval = setInterval(() => {
    wss.clients.forEach((ws) => {
      if (!ws._isAlive) {
        ws.terminate();
        return;
      }
      ws._isAlive = false;
      ws.ping();
    });
  }, 30_000);

  wss.on("close", () => clearInterval(heartbeatInterval));

  console.log("  [calls] WebSocket signaling: /calls/ws");
}

/**
 * Get info about an active room (for REST API).
 */
export function getRoomInfo(roomCode) {
  const room = _rooms.get(roomCode);
  if (!room) return null;
  const members = [];
  for (const [uid, peer] of room) {
    members.push({ uid, name: peer.profile.name, joinedAt: peer.joinedAt });
  }
  return { roomCode, participantCount: room.size, members };
}
