"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");
var _normalizeRoomJid = require("../helpers/normalizeRoomJid");
var _roomStore = require("../roomStore");
var _roomsSlice = require("../roomStore/roomsSlice");
var _outboundQueue = require("./outboundQueue");
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';

// @xmpp/client surfaces SASL/stream errors in a few shapes depending on
// where it fails — XMPPError on stream:error, SASLError on SASL bind, or
// a generic Error whose message contains "not-authorized". Match all of
// them so we trigger the credentials refresh path consistently.
function isNotAuthorizedError(err) {
  if (!err) {
    return false;
  }
  const condition = err?.condition || err?.name || err?.type || err?.error?.condition || '';
  if (typeof condition === 'string' && condition.includes('not-authorized')) {
    return true;
  }
  const message = (err?.message || err?.text || '').toString();
  return /not-authorized|SASLError/i.test(message);
}
class XmppClient {
  host = '';
  service = '';
  conference = '';
  status = 'offline';
  password = '';
  reconnectAttempts = 0;
  maxReconnectAttempts = 5;
  reconnectDelay = 2000;
  // Upper bound on the backoff delay. The old hard stop at
  // maxReconnectAttempts gave up entirely after ~62s; now we keep retrying
  // at a steady cadence on a long outage and just clamp the delay here.
  maxReconnectDelay = 30000;
  // Timestamp of the last forced reconnect so bursty external triggers
  // (NetInfo + AppState + the provider watchdog firing together) can't
  // spawn a reconnect storm.
  lastReconnectAt = 0;
  suppressReconnect = false;
  // Tracked so close()/disconnect() can cancel a pending reconnect —
  // otherwise a scheduleReconnect() timer fires after logout and (until
  // suppressReconnect short-circuits it) keeps the dead instance alive.
  reconnectTimer = null;
  // Single-flight guard for reconnect(). Multiple triggers can fire near-
  // simultaneously (AppState 'active' + NetInfo restore + a scheduled
  // timer). Without this, two overlapping reconnect()s each spawn a fresh
  // underlying client and one is leaked with its `stanza` handler still
  // bound → duplicate stanza dispatch into redux.
  reconnecting = false;

  // Set when SASL fails with `not-authorized` (typically a stale JWT-
  // derived XMPP password after idle). Triggers the credentialsProvider
  // refresh path in reconnect().
  lastAuthError = null;
  credentialsProvider = null;
  credentialsRefreshInFlight = null;
  // Fired by the provider on every 'online' so it can re-join MUC rooms
  // after a reconnect (bug #21 — reconnected but not in the room).
  onOnlineCallback = null;

  // ---- 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';
  }

  /**
   * Inject (or replace) the callback used by reconnect() to fetch fresh
   * XMPP credentials after a `not-authorized` SASL failure. The provider
   * is responsible for refreshing whatever upstream token is needed
   * (REST JWT, /users/client, etc.) and returning the resulting
   * username + password.
   */
  setCredentialsProvider(provider) {
    this.credentialsProvider = provider;
  }

  /** Install a callback invoked on every successful 'online'. The provider
   * uses it to re-join MUC rooms after a reconnect. */
  setOnOnline(cb) {
    this.onOnlineCallback = cb;
  }

  /** Swap in fresh credentials before the next reconnect. */
  updateCredentials(username, password) {
    this.username = username;
    this.password = password;
  }
  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';

      // Defensive: if a previous underlying client is still referenced
      // (initializeClient reached from a path other than reconnect()),
      // detach its handlers before we overwrite the ref so the orphaned
      // client can't keep dispatching stanzas into redux.
      try {
        this.detachEventListeners();
      } catch {}
      this.client = _client.default.client({
        service: url,
        username: (0, _walletUsername.walletToUsername)(this.username),
        password: this.password
      });
      try {
        this.client?.reconnect?.stop?.();
      } catch {}

      // 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 {}
            const result = origSend(stanza);
            // Most stanza helpers fire-and-forget `client.send(...)`
            // without awaiting/catching. On @xmpp/client builds where
            // send() returns a Promise, a transient send failure (e.g.
            // mid-reconnect) then surfaces as a red-screen "Uncaught (in
            // promise, id: N)" (#4). Attach a no-op catch so the
            // rejection is considered handled — callers that DO await/
            // catch still get it, because we return the SAME promise.
            if (result && typeof result.catch === 'function') {
              result.catch(() => {});
            }
            return result;
          };
          // 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);
        if (isNotAuthorizedError(error)) {
          this.lastAuthError = 'not-authorized';
        }
        this.status = 'error';
        // A connect-time failure lands in 'error', which — unlike a
        // 'disconnect' after being online — has no other retry path
        // (onError doesn't reschedule; the provider only reacts to
        // 'offline'). Schedule a backoff retry so a failed initial or
        // reconnect attempt recovers on its own. reconnect() refreshes
        // creds first when the failure was a SASL not-authorized.
        if (!this.suppressReconnect) {
          try {
            this.scheduleReconnect();
          } catch {}
        }
      });
    } catch (error) {
      console.error('Error initializing client:', error);
      this.status = 'error';
    }
  }

  // Stored so detachEventListeners() can remove exactly the handlers we
  // added (not xmpp.js's internal ones). Without this a re-login left the
  // previous instance's `stanza` handler bound, double-dispatching every
  // incoming stanza into the (new session's) redux store.

  attachEventListeners() {
    // Idempotent: drop any previously-attached handlers first so a
    // reconnect that re-runs initializeClient on a fresh underlying
    // client doesn't stack duplicates.
    this.detachEventListeners();
    this.onDisconnect = () => {
      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 {}
      // Self-heal at the socket level. The provider's reconnect effect
      // keys on `client.status`, but that's a mutated class field React
      // does NOT track — so a silent socket drop (e.g. JS paused by the
      // debugger, iOS WebSocket suspend) never re-runs the effect and the
      // client sits 'offline' forever (sends then queue and time out).
      // Driving reconnect here makes recovery independent of React.
      if (!this.suppressReconnect) {
        try {
          this.scheduleReconnect();
        } catch {}
      }
    };
    this.onOnline = () => {
      console.log('XMPP online.', new Date());
      this.status = 'online';
      this.presencesReady = true;
      this.reconnectAttempts = 0;
      try {
        (0, _devLogger.pushLog)('xmpp', 'online', this.username);
      } catch {}
      // Notify the provider that the session is live again. On a RECONNECT
      // (new stream) the client is no longer joined to any MUC, so the
      // provider must re-send room presences — otherwise messages get a
      // local double-tick but never reach the room (bug #21). Harmless on
      // the first connect (room list not loaded yet → joins nothing; the
      // bootstrap's own allRoomPresences handles that pass).
      try {
        this.onOnlineCallback?.();
      } catch (err) {
        console.warn('onOnline callback failed', err);
      }
      // Drain any sends that were buffered while the stream was down. The
      // presence stanzas from onOnlineCallback above were written to the
      // socket synchronously (allRoomPresences → presenceInRoom send the
      // <presence> before yielding), so flushing here puts the message
      // stanzas AFTER the joins on the same stream — XMPP's in-order
      // per-stream processing guarantees the MUC join lands first.
      try {
        this.flushPendingSends();
      } catch (err) {
        console.warn('flushPendingSends failed', err);
      }
    };
    this.onError = error => {
      console.error('XMPP client error:', error);
      if (isNotAuthorizedError(error)) {
        this.lastAuthError = 'not-authorized';
      }
      try {
        (0, _devLogger.pushLog)('xmpp', 'error', error && error.message || error);
      } catch {}
    };
    this.onStanza = 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)();
    };
    this.client.on('disconnect', this.onDisconnect);
    this.client.on('online', this.onOnline);
    this.client.on('error', this.onError);
    this.client.on('stanza', this.onStanza);
  }
  detachEventListeners() {
    const c = this.client;
    if (!c?.removeListener) {
      return;
    }
    try {
      if (this.onDisconnect) {
        c.removeListener('disconnect', this.onDisconnect);
      }
      if (this.onOnline) {
        c.removeListener('online', this.onOnline);
      }
      if (this.onError) {
        c.removeListener('error', this.onError);
      }
      if (this.onStanza) {
        c.removeListener('stanza', this.onStanza);
      }
    } catch {}
    this.onDisconnect = undefined;
    this.onOnline = undefined;
    this.onError = undefined;
    this.onStanza = undefined;
  }

  /**
   * 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.reconnecting || this.reconnectTimer) {
      return;
    }
    this.reconnectAttempts++;
    const exp = Math.min(this.reconnectAttempts - 1, Math.max(0, this.maxReconnectAttempts));
    const delay = Math.min(this.maxReconnectDelay, this.reconnectDelay * Math.pow(2, exp));
    console.log(`Reconnecting attempt ${this.reconnectAttempts} in ${delay}ms`);
    // No clearTimeout needed — the guard above guarantees no pending timer.
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      // The connection may have recovered on its own (xmpp.js auto-
      // reconnect) between scheduling and firing — don't tear down a
      // healthy stream. forceReconnect() bypasses this by calling
      // reconnect() directly.
      if (this.status === 'online') {
        this.reconnectAttempts = 0;
        return;
      }
      this.reconnect();
    }, delay);
  }

  /**
   * Force an immediate reconnect, resetting the backoff counter. Called
   * by the provider when NetInfo reports the network came back or the app
   * returned to the foreground — we don't want to wait out the existing
   * exponential delay (or a maxed-out attempt count) in those cases.
   */
  forceReconnect() {
    if (this.suppressReconnect) {
      return;
    }
    // Debounce bursts: NetInfo restore + AppState 'active' + the provider
    // watchdog can fire within the same instant. reconnect() dedups
    // CONCURRENT calls, but sequential ones (one finishes, the next
    // arrives) would still each tear down + re-create the client. Ignore
    // forces within 2s of the last one.
    const now = Date.now();
    if (now - this.lastReconnectAt < 2000) {
      return;
    }
    this.lastReconnectAt = now;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    this.reconnectAttempts = 0;
    this.reconnect();
  }
  async reconnect() {
    if (this.suppressReconnect) {
      return;
    }
    // Single-flight: ignore overlapping triggers (AppState + NetInfo +
    // scheduled timer) so we never spawn two underlying clients and leak
    // one with live listeners (duplicate stanza dispatch).
    if (this.reconnecting) {
      console.log('reconnect already in progress — skip');
      return;
    }
    this.reconnecting = true;
    console.log('Attempting to reconnect xmpp client...');
    try {
      // If the last failure was a SASL `not-authorized` (typically a stale
      // JWT-derived XMPP password after idle), try to fetch fresh creds
      // before retrying. Without this, reconnect retries with the same
      // stale password forever and the user has to kill the app.
      if (this.lastAuthError === 'not-authorized' && this.credentialsProvider) {
        try {
          await this.refreshCredentialsOnce();
        } catch (err) {
          console.warn('XMPP credential refresh failed; reconnecting with cached creds', err);
        }
      }

      // Tear the OLD underlying client down FULLY before spinning up a new
      // one: detach our handlers (so its `stanza` handler can't keep
      // dispatching) and stop it. detachEventListeners() must run while
      // `this.client` still points at the old client — i.e. before
      // initializeClient() reassigns it.
      const old = this.client;
      if (old) {
        this.detachEventListeners();
        try {
          await old.stop();
        } catch {}
      }
      this.initializeClient();
    } finally {
      this.reconnecting = false;
    }
  }

  /**
   * Single-flight wrapper around credentialsProvider. Multiple concurrent
   * reconnect() calls (e.g. from the provider's reconnect effect firing
   * twice quickly) share one in-flight request and update `this.password`
   * exactly once.
   */
  refreshCredentialsOnce() {
    if (this.credentialsRefreshInFlight) {
      return this.credentialsRefreshInFlight;
    }
    if (!this.credentialsProvider) {
      return Promise.resolve();
    }
    const provider = this.credentialsProvider;
    this.credentialsRefreshInFlight = (async () => {
      try {
        const fresh = await provider();
        if (fresh?.username && fresh?.password) {
          // Apply whatever the provider returns and retry. We do NOT try to
          // detect "auth expired" by comparing passwords — a stable
          // xmppPassword (e.g. deterministic from the JWT/wallet) is the
          // NORMAL case for a transient blip, and treating it as expiry
          // falsely bricked recoverable sessions (sending stopped). If the
          // creds really are stale we simply get not-authorized again and
          // retry, which is the confirmed-good 26.5.7 behaviour.
          this.updateCredentials(fresh.username, fresh.password);
          this.lastAuthError = null;
        }
      } finally {
        this.credentialsRefreshInFlight = null;
      }
    })();
    return this.credentialsRefreshInFlight;
  }
  async disconnect(options) {
    if (options?.suppressReconnect) {
      this.suppressReconnect = true;
    }
    return this.close();
  }
  async close() {
    // Cancel any pending reconnect so a stale timer can't resurrect this
    // instance after teardown/logout.
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    if (this.client) {
      // Detach OUR handlers first so a late stanza during the close
      // handshake can't re-enter handleStanza and dispatch into a store
      // that's being wiped (or, on re-login, the new session's store).
      this.detachEventListeners();
      try {
        await this.client.stop();
        console.log('XMPP client connection closed.');
      } catch (error) {
        console.error('Error closing the xmpp client:', error);
      }
    }
    this.status = 'offline';
    this.presencesReady = false;
    // Permanent teardown (logout / unmount) — drop buffered sends so they
    // don't replay into the next session. Reconnect uses old.stop(), NOT
    // close(), so the queue still survives a transient drop+reconnect.
    (0, _outboundQueue.clearOutboundSends)();
  }
  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)((0, _normalizeRoomJid.normalizeRoomJid)(roomJID, this.conference), this.client);
  };
  presenceInRoomStanza = roomJID => {
    (0, _presenceInRoom.presenceInRoom)(this.client, (0, _normalizeRoomJid.normalizeRoomJid)(roomJID, this.conference));
  };
  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, (0, _normalizeRoomJid.normalizeRoomJid)(roomJID, this.conference));
  }
  setRoomImageStanza = (roomJid, roomThumbnail, type, roomBackground) => {
    (0, _setRoomImage.setRoomImage)(roomJid, roomThumbnail, type, this.client, roomBackground);
  };
  getRoomInfoStanza = roomJID => {
    (0, _getRoomInfo.getRoomInfo)((0, _normalizeRoomJid.normalizeRoomJid)(roomJID, this.conference), this.client);
  };
  getRoomMembersStanza = roomJID => {
    (0, _getRoomMembers.getRoomMembers)((0, _normalizeRoomJid.normalizeRoomJid)(roomJID, this.conference), this.client);
  };

  /**
   * True when the underlying stream is live enough to actually put a
   * stanza on the wire. Used to gate sends: when false we buffer instead
   * of firing a fire-and-forget stanza into a dead/half-open socket
   * (which is silently dropped and only caught by the 30s watchdog).
   */
  isStreamReady() {
    return !!this.client && this.status === 'online' && !this.reconnecting;
  }

  /** Replay buffered sends against this (now-online) client, in order. */
  flushPendingSends() {
    (0, _outboundQueue.flushOutboundSends)(this, Date.now());
  }

  //messages
  sendMessage = (roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, customId) => {
    // Stream down (start race / reconnect window): buffer and replay on
    // the next 'online' instead of losing the stanza. Returns false so
    // callers know the send was deferred, not delivered.
    if (!this.isStreamReady()) {
      (0, _outboundQueue.enqueueOutboundSend)({
        optimisticId: customId || `send-text-message-${Date.now()}`,
        roomJID,
        enqueuedAt: Date.now(),
        send: c => c.sendMessage(roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, customId)
      });
      return false;
    }
    (0, _sendTextMessage.sendTextMessage)(this.client, roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, this.devServer || DEFAULT_DEV_SERVER, customId);
    return true;
  };
  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 {
      const markers = await (0, _getChatsPrivateStoreRequest.getChatsPrivateStoreRequest)(this.client);
      // Hydrate the fetched server-side read markers into redux. Without
      // this the markers were fetched on every init/reconnect and then
      // silently dropped, so any room the user hadn't locally opened+left
      // this session kept `lastViewedTimestamp: 0` and the unread
      // middleware's `> 0` gate skipped it forever — `useUnread()` stayed
      // at 0 with no badge. Done here on the single READ method so every
      // caller (provider init, reconnect, ChatWrapper, push handlers, …)
      // hydrates automatically and a future caller can't reintroduce the
      // drop. The write helpers call the lower-level
      // `getChatsPrivateStoreRequest` directly and intentionally skip this.
      if (markers && typeof markers === 'object') {
        const normalized = {};
        for (const jid of Object.keys(markers)) {
          const n = Number(markers[jid]);
          if (jid && Number.isFinite(n) && n > 0) {
            normalized[jid] = n;
          }
        }
        if (Object.keys(normalized).length > 0) {
          _roomStore.store.dispatch((0, _roomsSlice.applyPrivateStoreMarkers)(normalized));
        }
      }
      return markers;
    } 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) {
    // Media: the file is already uploaded by the time we get here; only the
    // stanza needs the stream. Buffer + replay on reconnect if it's down so
    // the upload isn't wasted (otherwise the stanza is lost and the media
    // bubble sits pending until the 30s watchdog fails it).
    if (!this.isStreamReady()) {
      (0, _outboundQueue.enqueueOutboundSend)({
        optimisticId: customId || `send-media-message-${Date.now()}`,
        roomJID,
        enqueuedAt: Date.now(),
        send: c => c.sendMediaMessageStanza(roomJID, data, customId)
      });
      return undefined;
    }
    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, customId) {
    // No translate tag support yet — fall back to a regular text send.
    // Forward the caller's customId so the optimistic message (heap/redux)
    // reconciles with the server echo. Without it, translated sends went
    // out with no client id → the echo carried a server-assigned id and
    // surfaced as a stuck-pending message PLUS a duplicate (affected both
    // the live translated-send path and resend).
    this.sendMessage(roomJID, firstName, lastName, photo, walletAddress, userMessage, notDisplayedValue, isReply, showInChannel, mainMessage, customId);
  }
}
exports.XmppClient = XmppClient;
var _default = exports.default = XmppClient;
//# sourceMappingURL=xmppClient.js.map