"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = exports.XmppClient = void 0;
var _client = _interopRequireDefault(require("@xmpp/client"));
var _walletUsername = require("../helpers/walletUsername");
var _sendMediaMessage = require("./xmpp/sendMediaMessage.xmpp");
var _getChatsPrivateStoreRequest = require("./xmpp/getChatsPrivateStoreRequest.xmpp");
var _actionSetTimestampToPrivateStore = require("./xmpp/actionSetTimestampToPrivateStore.xmpp");
var _flushLastViewedToPrivateStore = require("./xmpp/flushLastViewedToPrivateStore");
var _sendTypingRequest = require("./xmpp/sendTypingRequest.xmpp");
var _getHistory = require("./xmpp/getHistory.xmpp");
var _sendTextMessage = require("./xmpp/sendTextMessage.xmpp");
var _deleteMessage = require("./xmpp/deleteMessage.xmpp");
var _presenceInRoom = require("./xmpp/presenceInRoom.xmpp");
var _getLastMessageArchive = require("./xmpp/getLastMessageArchive.xmpp");
var _createRoom = require("./xmpp/createRoom.xmpp");
var _setRoomImage = require("./xmpp/setRoomImage.xmpp");
var _getRoomMembers = require("./xmpp/getRoomMembers.xmpp");
var _getRoomInfo = require("./xmpp/getRoomInfo.xmpp");
var _leaveTheRoom = require("./xmpp/leaveTheRoom.xmpp");
var _editMessage = require("./xmpp/editMessage.xmpp");
var _inviteRoomRequest = require("./xmpp/inviteRoomRequest.xmpp");
var _getRooms = require("./xmpp/getRooms.xmpp");
var _handleStanzas = require("./xmpp/handleStanzas.xmpp");
var _devLogger = require("../utils/devLogger");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
// Canonical production XMPP WSS endpoint (standard wss/443, no port suffix).
const DEFAULT_DEV_SERVER = 'xmpp.chat.ethora.com';
class XmppClient {
  host = '';
  service = '';
  conference = '';
  status = 'offline';
  password = '';
  reconnectAttempts = 0;
  maxReconnectAttempts = 5;
  reconnectDelay = 2000;
  suppressReconnect = false;

  // ---- QoS state (mirrors web XmppClient) ----------------------------
  presencesReady = false;
  disableLastRead = false;
  activeRoomJID = null;
  activeRoomBoostUntil = 0;
  softPauseUntil = 0;
  mamInFlightByRoom = new Map();
  maxInFlightHistory = 3;
  softPauseAfterSendMs = 250;
  activeRoomBoostTtlMs = 4000;
  alwaysPrioritizeActiveRoom = true;

  // -------------------------------------------------------------------

  checkOnline() {
    return this.client && this.client.status === 'online';
  }
  constructor(username, password, xmppSettings) {
    // Back-compat: third arg used to be a bare devServer string.
    if (typeof xmppSettings === 'string') {
      this.xmppSettings = {
        devServer: xmppSettings
      };
      this.devServer = xmppSettings;
    } else {
      this.xmppSettings = xmppSettings;
      this.devServer = xmppSettings?.devServer;
    }
    this.disableLastRead = this.xmppSettings?.disableLastRead === true;
    const qos = this.xmppSettings?.historyQoS;
    if (qos) {
      if (typeof qos.maxInFlightHistory === 'number') {
        this.maxInFlightHistory = qos.maxInFlightHistory;
      }
      if (typeof qos.softPauseAfterSendMs === 'number') {
        this.softPauseAfterSendMs = qos.softPauseAfterSendMs;
      }
      if (typeof qos.activeRoomBoostTtlMs === 'number') {
        this.activeRoomBoostTtlMs = qos.activeRoomBoostTtlMs;
      }
      if (typeof qos.alwaysPrioritizeActiveRoom === 'boolean') {
        this.alwaysPrioritizeActiveRoom = qos.alwaysPrioritizeActiveRoom;
      }
    }
    this.username = username;
    this.password = password;
    this.initializeClient();
  }

  // ===================================================================
  // QoS API — mirrors web XmppClientInterface
  // ===================================================================
  setActiveRoomJid(roomJID) {
    this.activeRoomJID = roomJID;
    if (roomJID) {
      this.promoteRoomHistory(roomJID);
    }
  }
  promoteRoomHistory(_roomJID) {
    this.activeRoomBoostUntil = Date.now() + this.activeRoomBoostTtlMs;
  }

  /**
   * Active-room gate is "open" whenever the boost has expired OR there's
   * room for more concurrent MAM fetches. Scheduler uses this to backoff.
   */
  isActiveRoomGateOpen() {
    if (this.mamInFlightByRoom.size >= this.maxInFlightHistory) {
      return false;
    }
    if (Date.now() < this.softPauseUntil) {
      return false;
    }
    return true;
  }

  /** Called right before a critical send. Soft-pauses background MAM. */
  onCriticalSend(roomJID, _messageId) {
    this.softPauseUntil = Date.now() + this.softPauseAfterSendMs;
    if (roomJID) {
      this.promoteRoomHistory(roomJID);
    }
  }

  /**
   * Higher-priority presence than presenceInRoomStanza. For now just
   * delegates — kept distinct so callers don't change when full priority
   * lanes land later.
   */
  async prioritizeRoomPresence(roomJID) {
    try {
      this.presenceInRoomStanza(roomJID);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Queued history fetch. Coalesces by room: if the same room already has
   * a higher-priority request in flight, return that promise. Honors
   * `skipIfPreloaded` by checking redux state (lazy import to avoid cycle).
   */
  async enqueueHistoryTask(params) {
    const {
      chatJID,
      max,
      before,
      id,
      source = 'default'
    } = params;

    // Coalesce — if a fetch for the same room is in-flight, reuse it
    // unless the new request is higher priority.
    const existing = this.mamInFlightByRoom.get(chatJID);
    if (existing && this.sourceRank(existing.source) <= this.sourceRank(source)) {
      return existing.promise;
    }

    // Wait for the gate (in-flight cap + soft-pause). For active-room
    // requests we still proceed past the cap when the boost is fresh.
    while (this.mamInFlightByRoom.size >= this.maxInFlightHistory && !(this.alwaysPrioritizeActiveRoom && this.activeRoomJID === chatJID)) {
      await new Promise(r => setTimeout(r, 80));
    }
    if (Date.now() < this.softPauseUntil && source === 'background') {
      const wait = this.softPauseUntil - Date.now();
      await new Promise(r => setTimeout(r, wait));
    }

    // Self-reference via an outer holder so the IIFE's `finally` can
    // identify "is the registry entry I started still the current one?"
    // without TS's TDZ false-positive (TS2454 on a bare `let promise`).
    const handle = {};
    handle.p = (async () => {
      try {
        return await (0, _getHistory.getHistory)(this.client, chatJID, max, before, id);
      } finally {
        const cur = this.mamInFlightByRoom.get(chatJID);
        if (cur && cur.promise === handle.p) {
          this.mamInFlightByRoom.delete(chatJID);
        }
      }
    })();
    this.mamInFlightByRoom.set(chatJID, {
      promise: handle.p,
      source,
      startedAt: Date.now()
    });
    return handle.p;
  }
  sourceRank(s) {
    switch (s) {
      case 'active':
        return 0;
      case 'send_ack':
        return 1;
      case 'default':
        return 2;
      case 'background':
        return 3;
    }
  }
  initializeClient() {
    try {
      const devServer = this.devServer || DEFAULT_DEV_SERVER;
      const url = `wss://${devServer}/ws`;
      this.service = url;
      this.host = this.xmppSettings?.host || url.match(/wss:\/\/([^:/]+)/)?.[1] || '';
      this.conference = this.xmppSettings?.conference || `conference.${this.host}`;
      this.status = 'connecting';
      this.client = _client.default.client({
        service: url,
        username: (0, _walletUsername.walletToUsername)(this.username),
        password: this.password
      });

      // Concurrent room preloads each register their own short-lived
      // 'stanza' handler (getHistory, getRooms, getChatsPrivateStore,
      // ...). With 5+ rooms preloading in parallel, Node's default
      // EventEmitter cap of 10 trips the "possible memory leak" warning
      // even though each handler is correctly unsubscribed in `finally`.
      // Raising the cap silences the warning without masking real leaks
      // — true leaks would still grow unboundedly past this number.
      try {
        this.client?.setMaxListeners?.(50);
      } catch {}

      // Wrap `send` so the dev logger sees outgoing stanzas too.
      // Guarded: some @xmpp/client builds define `send` as a getter or
      // non-writable property, and overwriting would throw silently and
      // strand the whole pipeline. We catch everything and fall back to
      // the unwrapped client.
      try {
        const origSend = this.client.send?.bind(this.client);
        if (origSend) {
          const wrapped = stanza => {
            try {
              const tag = stanza?.name || 'stanza';
              const id = stanza?.attrs?.id || '';
              const to = stanza?.attrs?.to || '';
              (0, _devLogger.pushLog)('xmpp', `→ ${tag}${id ? ` id=${id}` : ''}${to ? ` to=${to.split('/')[0]}` : ''}`, stanza?.toString ? stanza.toString() : undefined);
            } catch {}
            return origSend(stanza);
          };
          // Property may be non-writable on some builds; defineProperty
          // gives us a clearer error than a plain assignment.
          Object.defineProperty(this.client, 'send', {
            value: wrapped,
            writable: true,
            configurable: true
          });
        }
      } catch (err) {
        console.warn('XmppClient: could not wrap client.send for dev logging', err);
      }
      this.attachEventListeners();
      this.client.start().catch(error => {
        console.error('Error starting xmpp client:', error);
        this.status = 'error';
      });
    } catch (error) {
      console.error('Error initializing client:', error);
      this.status = 'error';
    }
  }
  attachEventListeners() {
    this.client.on('disconnect', () => {
      console.log('XMPP disconnected.');
      this.status = 'offline';
      try {
        // lazy-require to avoid pulling devLogger into prod bundles
        // that don't reference it; tree-shaken via dead-code elim.
        (0, _devLogger.pushLog)('xmpp', 'disconnect');
      } catch {}
    });
    this.client.on('online', () => {
      console.log('XMPP online.', new Date());
      this.status = 'online';
      this.presencesReady = true;
      this.reconnectAttempts = 0;
      try {
        (0, _devLogger.pushLog)('xmpp', 'online', this.username);
      } catch {}
    });
    this.client.on('error', error => {
      console.error('XMPP client error:', error);
      try {
        (0, _devLogger.pushLog)('xmpp', 'error', error && error.message || error);
      } catch {}
    });
    this.client.on('stanza', stanza => {
      try {
        const tag = stanza?.name || 'stanza';
        const id = stanza?.attrs?.id || '';
        const from = stanza?.attrs?.from || '';
        const type = stanza?.attrs?.type || '';
        (0, _devLogger.pushLog)('xmpp', `← ${tag}${id ? ` id=${id}` : ''}${type ? ` type=${type}` : ''}${from ? ` from=${from.split('/')[0]}` : ''}`, stanza?.toString ? stanza.toString() : undefined);
      } catch {}
      _handleStanzas.handleStanza.bind(this, stanza, this)();
    });
  }

  /**
   * Resolves once status === 'online' or rejects when status === 'error'.
   * Mirrors the web client's `ensureConnected` helper.
   */
  waitForOnline(timeoutMs = 15000) {
    if (this.status === 'online') {
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      const start = Date.now();
      const tick = () => {
        if (this.status === 'online') {
          return resolve();
        }
        if (this.status === 'error') {
          return reject(new Error('XMPP client error'));
        }
        if (Date.now() - start > timeoutMs) {
          return reject(new Error('XMPP connect timeout'));
        }
        setTimeout(tick, 250);
      };
      tick();
    });
  }

  // Alias to match the web XmppClientInterface.
  ensureConnected(timeout) {
    return this.waitForOnline(timeout);
  }
  scheduleReconnect() {
    if (this.suppressReconnect) {
      return;
    }
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnect attempts reached.');
      return;
    }
    this.reconnectAttempts++;
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
    console.log(`Reconnecting attempt ${this.reconnectAttempts} in ${delay}ms`);
    setTimeout(() => this.reconnect(), delay);
  }
  reconnect() {
    if (this.suppressReconnect) {
      return;
    }
    console.log('Attempting to reconnect xmpp client...');
    if (this.client) {
      this.client.stop().finally(() => {
        this.initializeClient();
      });
    } else {
      this.initializeClient();
    }
  }
  async disconnect(options) {
    if (options?.suppressReconnect) {
      this.suppressReconnect = true;
    }
    return this.close();
  }
  async close() {
    if (this.client) {
      try {
        await this.client.stop();
        console.log('XMPP client connection closed.');
      } catch (error) {
        console.error('Error closing the xmpp client:', error);
      }
    }
    this.status = 'offline';
  }
  getRoomsStanza = async () => {
    await (0, _getRooms.getRooms)(this.client);
  };

  //room functions

  async createRoomStanza(title, description, to) {
    return await (0, _createRoom.createRoom)(title, description, this.client);
  }
  async inviteRoomRequestStanza(to, roomJid) {
    await (0, _inviteRoomRequest.inviteRoomRequest)(this.client, to, roomJid);
  }
  leaveTheRoomStanza = roomJID => {
    (0, _leaveTheRoom.leaveTheRoom)(roomJID, this.client);
  };
  presenceInRoomStanza = roomJID => {
    (0, _presenceInRoom.presenceInRoom)(this.client, roomJID);
  };
  getHistoryStanza = async (chatJID, max, before, id, options) => {
    if (options?.coalesceRoom) {
      return this.enqueueHistoryTask({
        chatJID,
        max,
        before,
        id,
        source: options?.source || 'default'
      });
    }
    return await (0, _getHistory.getHistory)(this.client, chatJID, max, before, id);
  };
  getLastMessageArchiveStanza(roomJID) {
    (0, _getLastMessageArchive.getLastMessage)(this.client, roomJID);
  }
  setRoomImageStanza = (roomJid, roomThumbnail, type, roomBackground) => {
    (0, _setRoomImage.setRoomImage)(roomJid, roomThumbnail, type, this.client, roomBackground);
  };
  getRoomInfoStanza = roomJID => {
    (0, _getRoomInfo.getRoomInfo)(roomJID, this.client);
  };
  getRoomMembersStanza = roomJID => {
    (0, _getRoomMembers.getRoomMembers)(roomJID, this.client);
  };

  //messages
  sendMessage = (roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, customId) => {
    (0, _sendTextMessage.sendTextMessage)(this.client, roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, this.devServer || DEFAULT_DEV_SERVER, customId);
  };
  deleteMessageStanza(room, msgId) {
    (0, _deleteMessage.deleteMessage)(this.client, room, msgId);
  }
  editMessageStanza(room, msgId, text) {
    (0, _editMessage.editMessage)(this.client, room, msgId, text);
  }
  sendTypingRequestStanza(chatId, fullName, start) {
    (0, _sendTypingRequest.sendTypingRequest)(this.client, chatId, fullName, start);
  }
  getChatsPrivateStoreRequestStanza = async () => {
    if (this.disableLastRead) {
      return null;
    }
    try {
      return await (0, _getChatsPrivateStoreRequest.getChatsPrivateStoreRequest)(this.client);
    } catch (error) {
      console.log(error);
      return null;
    }
  };
  async actionSetTimestampToPrivateStoreStanza(chatId, timestamp, chats) {
    if (this.disableLastRead) {
      return;
    }
    try {
      await (0, _actionSetTimestampToPrivateStore.actionSetTimestampToPrivateStore)(this.client, chatId, timestamp, chats);
    } catch (error) {}
  }

  // Flush lastViewedTimestamp for every room into the server's private
  // store. Used by AppState/background and tab-change handlers — see
  // src/networking/xmpp/flushLastViewedToPrivateStore.ts for details.
  async flushLastViewedToPrivateStoreStanza(rooms, opts = {}) {
    if (this.disableLastRead) {
      return false;
    }
    try {
      return await (0, _flushLastViewedToPrivateStore.flushLastViewedToPrivateStore)(this, rooms, opts);
    } catch {
      return false;
    }
  }
  sendMediaMessageStanza(roomJID, data, customId) {
    return (0, _sendMediaMessage.sendMediaMessage)(this.client, roomJID, data, customId, this.devServer || DEFAULT_DEV_SERVER);
  }

  // -------------------------------------------------------------------
  // Method stubs to satisfy `XmppClientInterface`. Concrete RN xmpp
  // helpers don't exist yet — the stubs log a single warning and no-op
  // so consumers don't blow up at runtime. Replace with real impls when
  // their `*.xmpp.ts` helpers are ported from the web side.
  // -------------------------------------------------------------------

  setVCardStanza(_xmppUsername) {
    console.warn('setVCardStanza: not implemented in RN xmpp client');
  }
  async createPrivateRoomStanza(title, description, to) {
    console.warn('createPrivateRoomStanza: not implemented; falling back to createRoom');
    const result = await this.createRoomStanza(title, description, to);
    return typeof result === 'string' ? result : '';
  }
  sendMessageReactionStanza(_messageId, _roomJid, _reactionsList, _reactionSymbol) {
    console.warn('sendMessageReactionStanza: not implemented in RN xmpp client');
  }
  sendTextMessageWithTranslateTagStanza(roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, _langSource) {
    // No translate tag support yet — fall back to a regular text send.
    this.sendMessage(roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage);
  }
}
exports.XmppClient = XmppClient;
var _default = exports.default = XmppClient;
//# sourceMappingURL=xmppClient.js.map