// JamServer.js - Adapted for React app
// Wrapper around the WebSocket connection to the server
// Manages connection, heartbeats, and reconnect logic

import EventEmitter from 'events';

const ConnectionStatus = {
  DISCONNECTED: 'disconnected',
  CONNECTING: 'connecting',
  CONNECTED: 'connected',
  RECONNECTING: 'reconnecting',
  ERROR: 'error'
};

const MessageType = {
  LOGIN_ACK: 'login_ack',
  HEARTBEAT: 'heartbeat',
  HEARTBEAT_ACK: 'heartbeat_ack',
  SERVER_REJECTION_ERROR: 'server_rejection_error',
  PEER_MESSAGE: 'peer_message',
  LOGOUT: 'logout',
  CHAT_MESSAGE: 'chat_message',
  USER_STATUS: 'user_status'
};

class JamServer extends EventEmitter {
  constructor(options = {}) {
    super();

    this.app = options.app || {};
    this.activeElementEvent = options.activeElementEvent || (() => ({}));
    this.logger = options.logger || console;
    this.rest = options.rest || null;

    // Connection state
    this.channelId = null;
    this.clientType = null;
    this.mode = null;
    this.isLatencyTesterMode = false;

    // Heartbeat
    this.startHeartbeatTimeout = null;
    this.heartbeatInterval = null;
    this.heartbeatMS = null;
    this.connectionExpireTime = null;
    this.lastHeartbeatSentTime = null;
    this.lastHeartbeatAckTime = null;
    this.lastHeartbeatFound = false;
    this.lastDisconnectedReason = null;
    this.heartbeatAckCheckInterval = null;
    this.notificationLastSeenAt = undefined;
    this.notificationLastSeen = undefined;
    this.clientClosedConnection = false;
    this.initialConnectAttempt = true;
    this.active = true;

    // Reconnection logic
    this.connectDeferred = null;
    this.freezeInteraction = false;
    this.countdownInterval = null;
    this.reconnectAttemptLookup = [2, 2, 2, 4, 8, 15, 30];
    this.reconnectAttempt = 0;
    this.reconnectingWaitPeriodStart = null;
    this.reconnectDueTime = null;
    this.connectTimeout = null;
    this.activityTimeout = null;

    // Server state
    this.socket = null;
    this.signedIn = false;
    this.clientID = "";
    this.publicIP = "";
    this.dispatchTable = {};
    this.socketClosedListeners = [];
    this.connecting = false;
    this.connected = false;
    this.reconnecting = false;

    // WebSocket location for flash fallback (if needed)
    this.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";

    // Initialize
    this.initialize();
  }

  initialize() {
    this.registerLoginAck();
    this.registerHeartbeatAck();
    this.registerServerRejection();
    this.registerSocketClosed();
    this.activityCheck();
  }

  // Heartbeat functions
  heartbeatStateReset() {
    this.lastHeartbeatSentTime = null;
    this.lastHeartbeatAckTime = null;
    this.lastHeartbeatFound = false;
  }

  initiateReconnect(activeElementVotes, inError) {
    const initialConnect = !!activeElementVotes;

    this.freezeInteraction = activeElementVotes && (
      (activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) ||
      (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true)
    );

    if (inError) {
      this.reconnectAttempt = 0;
      this.emit('disconnected');
    }
  }

  closedCleanup(inError) {
    if (this.isLatencyTester()) {
      this.logger.info("latency-tester: websocket connection lost");
    }

    if (this.connected) {
      this.emit('connection_down');
    }

    this.connected = false;
    this.connecting = false;

    // Stop future heartbeats
    if (this.heartbeatInterval != null) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }

    // Stop heartbeat start delay
    if (this.startHeartbeatTimeout != null) {
      clearTimeout(this.startHeartbeatTimeout);
      this.startHeartbeatTimeout = null;
    }

    // Stop checking heartbeat acks
    if (this.heartbeatAckCheckInterval != null) {
      clearTimeout(this.heartbeatAckCheckInterval);
      this.heartbeatAckCheckInterval = null;
    }

    this.clearConnectTimeout();

    if (this.initialConnectAttempt || !this.reconnecting) {
      this.reconnecting = true;
      this.initialConnectAttempt = false;

      const result = this.activeElementEvent('beforeDisconnect');

      this.initiateReconnect(result, inError);

      this.activeElementEvent('afterDisconnect');

      // Notify listeners
      const len = this.socketClosedListeners.length;
      for (let i = 0; i < len; i++) {
        try {
          this.socketClosedListeners[i](inError);
        } catch (ex) {
          this.logger.warn('exception in callback for websocket closed event:' + ex);
        }
      }
    }
  }

  _heartbeatAckCheck() {
    if (this.lastHeartbeatFound) {
      return;
    }

    if (new Date().getTime() - this.lastHeartbeatAckTime.getTime() > this.connectionExpireTime) {
      this.logger.error("no heartbeat ack received from server after ", this.connectionExpireTime, " seconds. giving up on socket connection");
      this.lastDisconnectedReason = 'NO_HEARTBEAT_ACK';
      this.close(true);
    } else {
      this.lastHeartbeatFound = true;
    }
  }

  _heartbeat() {
    if (this.isLatencyTester()) {
      this.logger.info("latency-tester debug: heartbeat" + this.app.heartbeatActive);
    }

    if (this.app.heartbeatActive) {
      const message = this.createHeartbeatMessage(
        this.notificationLastSeen,
        this.notificationLastSeenAt,
        this.active
      );
      this.notificationLastSeenAt = undefined;
      this.notificationLastSeen = undefined;

      const now = new Date();
      if (this.lastHeartbeatSentTime) {
        const drift = new Date().getTime() - this.lastHeartbeatSentTime.getTime() - this.heartbeatMS;
        if (drift > 500) {
          this.logger.warn("significant drift between heartbeats: " + drift + 'ms beyond target interval');
        }
      }
      this.lastHeartbeatSentTime = now;
      this.send(message);
      this.lastHeartbeatFound = false;
    }
  }

  isClientMode() {
    return this.mode == "client";
  }

  isLatencyTester() {
    return this.isLatencyTesterMode;
  }

  clearConnectTimeout() {
    if (this.connectTimeout) {
      clearTimeout(this.connectTimeout);
      this.connectTimeout = null;
    }
  }

  loggedIn(header, payload) {
    setTimeout(() => {
      this.signedIn = true;
      this.clientID = payload.client_id;
      this.publicIP = payload.public_ip;

      if (typeof window !== 'undefined' && window.jamClient !== undefined) {
        window.jamClient.connected = true;
        window.jamClient.clientID = this.clientID;
      }

      this.clearConnectTimeout();

      this.heartbeatStateReset();

      this.app.clientId = payload.client_id;

      if (this.isClientMode() && typeof window !== 'undefined' && window.jamClient) {
        try {
          const msg = {
            user_id: payload.user_id,
            token: payload.token,
            username: payload.username,
            arses: payload.arses,
            client_id_int: payload.client_id_int,
            subscription: payload.subscription
          };
          if (payload.connection_policy) {
            try {
              msg.policy = JSON.parse(payload.connection_policy);
            } catch (e) {
              msg.policy = null;
              console.log("unable to parse connection policy", e);
            }
          }
          console.log("logged with new msg", msg);
          if (window.jamClient.OnLoggedIn) {
            window.jamClient.OnLoggedIn(msg);
          }
        } catch (e) {
          console.log("fallback to old callback", e);
          if (window.jamClient.OnLoggedIn) {
            window.jamClient.OnLoggedIn(payload.user_id, payload.token, payload.username);
          }
        }
      }

      this.heartbeatMS = payload.heartbeat_interval * 1000;
      this.connectionExpireTime = payload.connection_expire_time * 1000;
      this.logger.info("loggedIn(): clientId=" + this.app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's');

      const randomStartTime = this.connectionExpireTime * (Math.random() / 2);

      if (this.startHeartbeatTimeout) {
        this.logger.warn("start heartbeat timeout is active; should be null");
        clearTimeout(this.startHeartbeatTimeout);
      }

      if (this.heartbeatInterval != null) {
        this.logger.warn("heartbeatInterval is active; should be null");
        clearInterval(this.heartbeatInterval);
        this.heartbeatInterval = null;
      }

      if (this.heartbeatAckCheckInterval != null) {
        this.logger.warn("heartbeatAckCheckInterval is active; should be null");
        clearInterval(this.heartbeatAckCheckInterval);
        this.heartbeatAckCheckInterval = null;
      }

      this.startHeartbeatTimeout = setTimeout(() => {
        if (this.connected) {
          this.heartbeatInterval = setInterval(() => this._heartbeat(), this.heartbeatMS);
          this.heartbeatAckCheckInterval = setInterval(() => this._heartbeatAckCheck(), 1000);
          this.lastHeartbeatAckTime = new Date(new Date().getTime() + this.heartbeatMS);
        }
      }, randomStartTime);

      this.logger.info("starting heartbeat timer in " + randomStartTime / 1000 + 's');

      this.connectDeferred.resolve();
      this.emit('connection_up');

      this.activeElementEvent('afterConnect', payload);

      if (payload.client_update && typeof window !== 'undefined' && window.JK && window.JK.ClientUpdateInstance) {
        window.JK.ClientUpdateInstance.runCheck(
          payload.client_update.product,
          payload.client_update.version,
          payload.client_update.uri,
          payload.client_update.size
        );
      }
    }, 0);
  }

  setActive(active) {
    if (typeof window !== 'undefined' && window.UserActivityActions) {
      window.UserActivityActions.setActive(active);
    }
  }

  markAway() {
    this.logger.debug("sleep again!");
    this.active = false;
    this.setActive(this.active);
    const userStatus = this.createUserStatusMessage(false, null);
    this.send(userStatus);
  }

  activityCheck() {
    const timeoutTime = 300000; // 5 minutes
    this.active = true;
    this.setActive(this.active);
    this.activityTimeout = setTimeout(() => this.markAway(), timeoutTime);

    if (typeof document !== 'undefined') {
      document.addEventListener('mousedown', this.handleActivity.bind(this));
      document.addEventListener('keydown', this.handleActivity.bind(this));
      document.addEventListener('touchstart', this.handleActivity.bind(this));
      document.addEventListener('focus', this.handleActivity.bind(this));
    }
  }

  handleActivity() {
    if (this.activityTimeout) {
      clearTimeout(this.activityTimeout);
      this.activityTimeout = null;
    }

    if (!this.active) {
      if (this.connected) {
        this.logger.debug("awake again!");
        const userStatus = this.createUserStatusMessage(true, null);
        this.send(userStatus);
      }
    }
    this.active = true;
    this.setActive(this.active);
    this.activityTimeout = setTimeout(() => this.markAway(), 300000);
  }

  heartbeatAck(header, payload) {
    this.lastHeartbeatAckTime = new Date();
  }

  registerLoginAck() {
    this.logger.debug("register for loggedIn to set clientId");
    this.registerMessageCallback(MessageType.LOGIN_ACK, this.loggedIn.bind(this));
  }

  registerHeartbeatAck() {
    this.logger.debug("register for heartbeatAck");
    this.registerMessageCallback(MessageType.HEARTBEAT_ACK, this.heartbeatAck.bind(this));
  }

  registerSocketClosed() {
    this.logger.debug("register for socket closed");
    this.registerOnSocketClosed(this.socketClosed.bind(this));
  }

  registerServerRejection() {
    this.logger.debug("register for server rejection");
    this.registerMessageCallback(MessageType.SERVER_REJECTION_ERROR, this.serverRejection.bind(this));
  }

  socketClosed(inError) {
    if (typeof window !== 'undefined' && window.jamClient) {
      if (window.jamClient.OnLoggedOut) {
        window.jamClient.OnLoggedOut();
      }
    }
  }

  serverRejection(header, payload) {
    this.logger.warn("server rejected our websocket connection. reason=" + payload.error_msg);

    if (payload.error_code == 'max_user_connections') {
      this.emit('alert', "Too Many Connections", "You have too many connections to the server. If you believe this is in error, please contact support.");
    } else if (payload.error_code == 'invalid_login' || payload.error_code == 'empty_login') {
      this.logger.debug(payload.error_code + ": no longer reconnecting");
      this.noReconnect = true;
    } else if (payload.error_code == 'no_reconnect') {
      this.logger.debug(payload.error_code + ": no longer reconnecting");
      this.noReconnect = true;
      this.emit('alert', "Misbehaved Client", "Please restart your application in order to continue using JamKazam.");
    }
  }

  // Reconnection functions
  internetUp() {
    const start = new Date().getTime();
    this.connect()
      .done(() => {
        this.guardAgainstRapidTransition(start, this.finishReconnect.bind(this));
      })
      .fail(() => {
        this.guardAgainstRapidTransition(start, this.closedOnReconnectAttempt.bind(this));
      });
  }

  closedOnReconnectAttempt() {
    this.failedReconnect();
  }

  finishReconnect() {
    if (!this.clientClosedConnection) {
      this.lastDisconnectedReason = 'WEBSOCKET_CLOSED_REMOTELY';
      this.clientClosedConnection = false;
    } else if (!this.lastDisconnectedReason) {
      this.lastDisconnectedReason = 'WEBSOCKET_CLOSED_LOCALLY';
    }

    this.emit('reconnected');
    this.connected = true;
    this.reconnecting = false;
    this.connecting = false;
    this.initialConnectAttempt = false;
  }

  guardAgainstRapidTransition(start, nextStep) {
    const now = new Date().getTime();
    if ((now - start) < 1500) {
      setTimeout(() => {
        nextStep();
      }, 1500 - (now - start));
    } else {
      nextStep();
    }
  }

  failedReconnect() {
    this.reconnectAttempt += 1;
    this.emit('reconnect_failed');
    this.beginReconnectPeriod();
  }

  attemptReconnect() {
    if (this.connecting) {
      this.logger.warn("attemptReconnect called when already connecting");
      return;
    }

    if (this.connected) {
      this.logger.warn("attemptReconnect called when already connected");
      return;
    }

    const start = new Date().getTime();
    this.emit('reconnecting');

    if (this.rest && this.rest.serverHealthCheck) {
      this.rest.serverHealthCheck()
        .done(() => {
          this.guardAgainstRapidTransition(start, this.internetUp.bind(this));
        })
        .fail((xhr, textStatus, errorThrown) => {
          if (xhr && xhr.status >= 100) {
            this.guardAgainstRapidTransition(start, this.internetUp.bind(this));
          } else {
            this.guardAgainstRapidTransition(start, this.failedReconnect.bind(this));
          }
        });
    } else {
      this.guardAgainstRapidTransition(start, this.internetUp.bind(this));
    }

    return false;
  }

  clearReconnectTimers() {
    if (this.countdownInterval) {
      clearInterval(this.countdownInterval);
      this.countdownInterval = null;
    }
  }

  beginReconnectPeriod() {
    this.reconnectingWaitPeriodStart = new Date().getTime();
    this.reconnectDueTime = this.reconnectingWaitPeriodStart + this.reconnectDelaySecs() * 1000;

    this.countdownInterval = setInterval(() => {
      const now = new Date().getTime();
      if (now > this.reconnectDueTime) {
        this.clearReconnectTimers();
        this.attemptReconnect();
      } else {
        const secondsUntilReconnect = Math.ceil((this.reconnectDueTime - now) / 1000);
        this.emit('reconnect_countdown', secondsUntilReconnect);
      }
    }, 333);
  }

  reconnectDelaySecs() {
    if (this.reconnectAttempt > this.reconnectAttemptLookup.length - 1) {
      return this.reconnectAttemptLookup[this.reconnectAttemptLookup.length - 1];
    } else {
      return this.reconnectAttemptLookup[this.reconnectAttempt];
    }
  }

  // Public API methods
  registerOnSocketClosed(callback) {
    this.socketClosedListeners.push(callback);
  }

  registerMessageCallback(messageType, callback) {
    console.log("JamServer.registerMessageCallback", messageType, callback);
    if (this.dispatchTable[messageType] === undefined) {
      this.dispatchTable[messageType] = [];
    }
    this.dispatchTable[messageType].push(callback);
  }

  unregisterMessageCallback(messageType, callback) {
    if (this.dispatchTable[messageType] !== undefined) {
      for (let i = this.dispatchTable[messageType].length; i--;) {
        if (this.dispatchTable[messageType][i] === callback) {
          this.dispatchTable[messageType].splice(i, 1);
          break;
        }
      }

      if (this.dispatchTable[messageType].length === 0) {
        delete this.dispatchTable[messageType];
      }
    }
  }

  connect() {
    if (this.connecting) {
      this.logger.error("connect should never be called if we are already connecting. cancelling.");
      return Promise.reject(new Error("Already connecting"));
    }

    if (this.connected) {
      this.logger.error("connect should never be called if we are already connected. cancelling.");
      return Promise.reject(new Error("Already connected"));
    }

    if (!this.clientType) {
      this.clientType = (typeof window !== 'undefined' && window.JK && window.JK.clientType) ? window.JK.clientType() : 'web';
    }

    if (!this.mode) {
      this.mode = 'client';
      if (typeof window !== 'undefined' && window.jamClient && window.jamClient.getOperatingMode) {
        this.mode = window.jamClient.getOperatingMode();
      }
      this.isLatencyTesterMode = this.mode == 'server';
    }

    this.connectDeferred = {};
    this.connectDeferred.promise = new Promise((resolve, reject) => {
      this.connectDeferred.resolve = resolve;
      this.connectDeferred.reject = reject;
    });

    this.channelId = this.generateUUID();

    let rememberToken = null;
    if (typeof window !== 'undefined' && window.jQuery && window.jQuery.cookie) {
      rememberToken = window.jQuery.cookie("remember_token");
    }

    if (this.isClientMode() && !rememberToken) {
      this.noReconnect = true;
      this.logger.debug("no login info; shutting down websocket");
      this.emit('login_required');
      this.connectDeferred.reject(new Error("No login token"));
      return this.connectDeferred.promise;
    }

    let client_type = this.clientType;
    let client_id = null;
    let machine = null;

    if (this.isClientMode()) {
      if (typeof window !== 'undefined' && window.gon && window.gon.global && window.gon.global.env == "development") {
        if (window.jQuery && window.jQuery.cookie) {
          client_id = window.jQuery.cookie('client_id');
        }
      }
      if (typeof window !== 'undefined' && window.jamClient && window.jamClient.SessionGetMacHash) {
        machine = window.jamClient.SessionGetMacHash();
        if (machine) {
          machine = machine.all;
        }
      }
    } else {
      client_type = 'latency_tester';
      if (typeof window !== 'undefined' && window.jamClient) {
        client_id = window.jamClient.clientID;
      }
      machine = null;
    }

    const params = {
      channel_id: this.channelId,
      token: rememberToken,
      client_type: client_type,
      client_id: client_id,
      machine: machine,
      os: (typeof window !== 'undefined' && window.JK && window.JK.GetOSAsString) ? window.JK.GetOSAsString() : 'unknown',
      product: "JamClientModern"
    };

    let uri = 'ws://localhost:8080'; // Default fallback
    if (typeof window !== 'undefined' && window.gon && window.gon.websocket_gateway_uri) {
      uri = window.gon.websocket_gateway_uri + '?' + new URLSearchParams(params).toString();
    }

    this.logger.debug("connecting websocket: " + uri);

    this.connecting = true;
    this.socket = new WebSocket(uri);
    this.socket.channelId = this.channelId;
    this.socket.onopen = this.onOpen.bind(this);
    this.socket.onmessage = this.onMessage.bind(this);
    this.socket.onclose = this.onClose.bind(this);

    this.connectTimeout = setTimeout(() => {
      this.logger.debug("connection timeout fired");
      this.connectTimeout = null;

      if (this.connectDeferred && this.connectDeferred.reject) {
        this.close(true);
        this.connectDeferred.reject(new Error("Connection timeout"));
      }
    }, 4000);

    return this.connectDeferred.promise;
  }

  close(inError) {
    this.logger.info("closing websocket");

    this.clientClosedConnection = true;
    if (this.socket) {
      this.socket.close();
    }

    this.closedCleanup(inError);
  }

  rememberLogin() {
    let token;
    if (typeof window !== 'undefined' && window.jQuery && window.jQuery.cookie) {
      token = window.jQuery.cookie("remember_token");
    }

    const loginMessage = this.createLoginWithTokenMessage(token, null, this.clientType);
    this.send(loginMessage);
  }

  latencyTesterLogin() {
    if (typeof window !== 'undefined' && window.jamClient) {
      const loginMessage = this.createLoginWithClientIdMessage(window.jamClient.clientID, 'latency_tester');
      this.send(loginMessage);
    }
  }

  onOpen() {
    this.logger.debug("JamServer.onOpen");
  }

  onMessage(e) {
    let message;
    try {
      message = JSON.parse(e.data);
    } catch (error) {
      this.logger.error("Failed to parse WebSocket message:", error);
      return;
    }

    const messageType = message.type.toLowerCase();
    const payload = message[messageType];
    const callbacks = this.dispatchTable[message.type];

    if (message.type == MessageType.PEER_MESSAGE) {
      // Logger info for peer messages (commented out for performance)
    } else if (message.type != MessageType.HEARTBEAT_ACK && message.type != MessageType.PEER_MESSAGE) {
      this.logger.info("JamServer.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
    }

    if (callbacks !== undefined) {
      const len = callbacks.length;
      for (let i = 0; i < len; i++) {
        try {
          callbacks[i](message, payload);
        } catch (ex) {
          this.logger.warn('exception in callback for websocket message:' + ex);
          throw ex;
        }
      }
    } else {
      this.logger.info("Unexpected message type %s.", message.type);
    }
  }

  onClose() {
    this.logger.info("Socket to server closed.");

    const disconnectedSocket = this.socket;

    if (disconnectedSocket.channelId != this.socket.channelId) {
      this.logger.debug(" ignoring disconnect for non-current socket. current=" + this.socket.channelId + ", disc=" + disconnectedSocket.channelId);
      return;
    }

    if (this.connectDeferred && this.connectDeferred.reject) {
      this.connectDeferred.reject(new Error("WebSocket closed"));
    }

    this.closedCleanup(true);
  }

  send(message) {
    const jsMessage = JSON.stringify(message);

    console.log("JamServer.send(" + jsMessage + ")");

    if (this.isLatencyTester() && (message.type == MessageType.HEARTBEAT || message.type == MessageType.PEER_MESSAGE)) {
      this.logger.info("latency-tester: JamServer.send(" + jsMessage + ")");
    } else if (message.type != MessageType.HEARTBEAT && message.type != MessageType.PEER_MESSAGE) {
      this.logger.info("JamServer.send(" + jsMessage + ")");
    }

    if (this.socket && this.socket.send !== undefined) {
      try {
        this.socket.send(jsMessage);
      } catch (err) {
        this.logger.warn("error when sending on websocket: " + err);
      }
    } else {
      this.logger.warn("Dropped message because server connection is closed.");
    }
  }

  loginSession(sessionId) {
    if (!this.signedIn) {
      this.logger.warn("Not signed in!");
      return;
    }

    const loginMessage = this.createLoginJamSessionMessage(sessionId);
    this.send(loginMessage);
  }

  sendP2PMessage(receiver_id, message) {
    const outgoing_msg = this.createClientP2PMessage(this.clientID, receiver_id, message);
    this.send(outgoing_msg);
  }

  sendLogin(token) {
    this.logger.debug("sending login message on behalf of client");
    const outgoing_msg = this.createLoginWithTokenMessage(token, null, null);
    this.send(outgoing_msg);
  }

  sendLogout() {
    this.logger.debug("sending logout message on behalf of client");
    const outgoing_msg = this.createLogoutMessage();
    this.send(outgoing_msg);
  }

  sendChatMessage(channel, message) {
    if (this.connected) {
      const chatMsg = this.createChatMessage(channel, message);
      this.send(chatMsg);
      return true;
    } else {
      return false;
    }
  }

  updateNotificationSeen(notificationId, notificationCreatedAt) {
    const time = new Date(notificationCreatedAt);

    if (!notificationCreatedAt) {
      throw 'invalid value passed to updateNotificationSeen';
    }

    if (!this.notificationLastSeenAt) {
      this.notificationLastSeenAt = notificationCreatedAt;
      this.notificationLastSeen = notificationId;
      this.logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
    } else if (time.getTime() > new Date(this.notificationLastSeenAt).getTime()) {
      this.notificationLastSeenAt = notificationCreatedAt;
      this.notificationLastSeen = notificationId;
      this.logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
    } else {
      this.logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
    }
  }

  // Message factory methods (simplified versions)
  createHeartbeatMessage(notificationLastSeen, notificationLastSeenAt, active) {
    return {
      type: MessageType.HEARTBEAT,
      heartbeat: {
        notification_last_seen: notificationLastSeen,
        notification_last_seen_at: notificationLastSeenAt,
        active: active
      }
    };
  }

  createUserStatusMessage(active, status) {
    return {
      type: MessageType.USER_STATUS,
      user_status: {
        active: active,
        status: status
      }
    };
  }

  createLoginWithTokenMessage(token, clientId, clientType) {
    return {
      type: 'login_with_token',
      login_with_token: {
        token: token,
        client_id: clientId,
        client_type: clientType
      }
    };
  }

  createLoginWithClientIdMessage(clientId, clientType) {
    return {
      type: 'login_with_client_id',
      login_with_client_id: {
        client_id: clientId,
        client_type: clientType
      }
    };
  }

  createLoginJamSessionMessage(sessionId) {
    return {
      type: 'login_jam_session',
      login_jam_session: {
        session_id: sessionId
      }
    };
  }

  createClientP2PMessage(senderId, receiverId, message) {
    return {
      type: MessageType.PEER_MESSAGE,
      peer_message: {
        from: senderId,
        to: receiverId,
        message: message
      }
    };
  }

  createLogoutMessage() {
    return {
      type: MessageType.LOGOUT,
      logout: {}
    };
  }

  createChatMessage(channel, message) {
    return {
      type: MessageType.CHAT_MESSAGE,
      chat_message: {
        channel: channel,
        message: message
      }
    };
  }

  // Utility methods
  generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0;
      const v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }

  // Getters
  getConnectionStatus() {
    if (this.connected) return ConnectionStatus.CONNECTED;
    if (this.connecting) return ConnectionStatus.CONNECTING;
    if (this.reconnecting) return ConnectionStatus.RECONNECTING;
    return ConnectionStatus.DISCONNECTED;
  }
}

// Register peer message handler
JamServer.prototype.registerMessageCallback(MessageType.PEER_MESSAGE, function(header, payload) {
  if (typeof window !== 'undefined' && window.jamClient !== undefined) {
    if (window.jamClient.P2PMessageReceived) {
      window.jamClient.P2PMessageReceived(header.from, payload.message);
    }
  }
});

export default JamServer;
