"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.updateRoom = exports.setRoomRole = exports.setRoomNoMessages = exports.setRoomMessages = exports.setReactions = exports.setPendingNotificationJid = exports.setLogoutState = exports.setLastViewedTimestamp = exports.setIsLoading = exports.setEditAction = exports.setCurrentRoom = exports.setComposing = exports.setCloseActiveMessage = exports.setActiveMessage = exports.roomsStore = exports.msgSortableMs = exports.editRoomMessage = exports.deleteRoomMessage = exports.deleteRoom = exports.deleteAllRooms = exports.default = exports.clearPendingNotificationJid = exports.applyRoomsPreloadBatch = exports.addRoomViaApi = exports.addRoomMessage = exports.addRoom = void 0;
var _toolkit = require("@reduxjs/toolkit");
var _insertMessageWithDelimiter = require("../helpers/insertMessageWithDelimiter");
// Per-room runtime message cap. Mirrors the persistence layer's
// MESSAGE_LIMIT so what's in memory matches what's on disk; otherwise
// long sessions grow the array unbounded (memory leak) and reload
// shows fewer messages than the user saw last in the session.
//
// The cap fires on the APPEND path (new messages arriving) — paginated
// older history added via unshift/splice is exempt, so the user can
// page backward arbitrarily far without instantly losing what they
// just fetched. After cap eviction we drop from the head (oldest).
const RUNTIME_MESSAGE_LIMIT = 100;
const enforceMessageCap = messages => {
  // Trim oldest until we're at/under the limit. Mutates in place
  // (immer-compatible inside reducers).
  while (messages.length > RUNTIME_MESSAGE_LIMIT) {
    messages.shift();
  }
};
const initialState = {
  rooms: {},
  activeRoomJID: null,
  isLoading: false,
  editAction: {
    isEdit: false,
    roomJid: '',
    messageId: '',
    text: ''
  },
  pendingNotificationJid: null
};
const isValidRoomJid = jid => {
  if (typeof jid !== 'string' || !jid) return false;
  if (!jid.includes('@')) return false;
  return true;
};
const addRoomViaApi = exports.addRoomViaApi = (0, _toolkit.createAsyncThunk)('roomMessages/addRoomViaApi', async ({
  room,
  xmpp: _xmpp
}, {
  dispatch
}) => {
  if (!room || !room.jid) return;
  dispatch(roomsStore.actions.addRoomFromApi({
    room
  }));
});

// Reducers extracted so the slice can carry an explicit
// Slice<State, typeof reducers, Name> annotation, which prevents tsc
// from inlining immer's internal WritableNonArrayDraft type into the
// emitted .d.ts (TS4023). See chatSettingsSlice.ts for the same pattern.
const reducers = {
  addRoom(state, action) {
    const {
      roomData
    } = action.payload;
    const existing = state.rooms[roomData.jid];
    // Preserve a pre-existing lastViewedTimestamp (the private-store
    // pull may have landed before /chats/my finished). If neither
    // source set one, stamp `Date.now()` so initial REST history
    // counts as "seen" — without this default, every back-history
    // message read by the unread middleware satisfies `>0` and the
    // badge shows the entire room population on first paint.
    const lastViewed = roomData.lastViewedTimestamp ?? existing?.lastViewedTimestamp ?? Date.now();
    state.rooms[roomData.jid] = {
      ...roomData,
      lastViewedTimestamp: lastViewed
    };
  },
  deleteRoom(state, action) {
    const {
      jid
    } = action.payload;
    if (state.rooms[jid]) {
      delete state.rooms[jid];
    }
  },
  updateRoom(state, action) {
    const {
      jid,
      updates
    } = action.payload;
    const existingRoom = state.rooms[jid];
    if (existingRoom) {
      state.rooms[jid] = {
        ...existingRoom,
        ...updates
      };
    }
  },
  setRoomMessages(state, action) {
    const {
      roomJID,
      messages
    } = action.payload;
    if (state.rooms[roomJID]) {
      // Cap to the runtime limit on replace too — guards against a
      // single MAM page returning more than the limit (would balloon
      // the array on its own).
      const capped = messages.length > RUNTIME_MESSAGE_LIMIT ? messages.slice(-RUNTIME_MESSAGE_LIMIT) : messages;
      state.rooms[roomJID].messages = capped;
    }
  },
  deleteRoomMessage(state, action) {
    const {
      roomJID,
      messageId
    } = action.payload;
    if (state.rooms[roomJID]) {
      state.rooms[roomJID].messages.map(message => {
        if (message.id === messageId) {
          message.isDeleted = true;
        }
      });
    }
  },
  setEditAction: (state, action) => {
    if (action.payload?.isEdit) {
      state.editAction = action.payload;
    } else {
      state.editAction = {
        isEdit: false,
        roomJid: '',
        messageId: '',
        text: ''
      };
    }
  },
  editRoomMessage(state, action) {
    const {
      roomJID,
      messageId,
      text
    } = action.payload;
    if (state.rooms[roomJID]) {
      state.rooms[roomJID].messages.map(message => {
        if (message.id === messageId) {
          message.body = text;
        }
      });
    }
  },
  addRoomMessage(state, action) {
    const {
      roomJID,
      message,
      start
    } = action.payload;

    // Guard against the "stanza arrived before /chats/my completed"
    // race — without this, the optional-chain `?.messages` falls
    // back to undefined, and the assignment below tries to set
    // `.messages` on undefined → TypeError. Caller can safely
    // ignore the message; the room will get its messages array on
    // `addRoom` when the API response lands.
    if (!state.rooms[roomJID]) {
      return;
    }
    if (!state.rooms[roomJID].messages) {
      state.rooms[roomJID].messages = [];
    }
    const roomMessages = state.rooms[roomJID].messages;
    const lengthBefore = roomMessages.length;
    if (roomMessages.length === 0 || start) {
      roomMessages.unshift(message);
    } else {
      const lastViewedTimestamp = state.rooms[roomJID].lastViewedTimestamp ? new Date(state.rooms[roomJID].lastViewedTimestamp) : null;
      (0, _insertMessageWithDelimiter.insertMessageWithDelimiter)(roomMessages, message, lastViewedTimestamp);
    }

    // Apply the in-memory cap only when the array actually GREW
    // (i.e. this wasn't a dedupe/merge that left length unchanged).
    // Otherwise repeated echoes of the same message would chip away
    // at the oldest history for no reason.
    if (roomMessages.length > lengthBefore) {
      enforceMessageCap(roomMessages);
    }
  },
  deleteAllRooms(state) {
    state.rooms = {};
  },
  setComposing(state, action) {
    const {
      chatJID,
      composing,
      composingList
    } = action.payload;
    state.rooms[chatJID].composing = composing;
    state.rooms[chatJID].composingList = composingList;
  },
  setIsLoading: (state, action) => {
    const {
      chatJID,
      loading,
      loadingText
    } = action.payload;
    if (chatJID && state.rooms?.[chatJID]) {
      state.rooms[chatJID].isLoading = loading;
    }
    state.isLoading = loading;
    state.loadingText = loadingText;
  },
  setLastViewedTimestamp: (state, action) => {
    const {
      chatJID,
      timestamp
    } = action.payload;
    if (state.rooms[chatJID]) {
      state.rooms[chatJID].lastViewedTimestamp = timestamp;
      // timestamp === 0 means "user is currently viewing this room" —
      // unread is cleared. Otherwise count messages received strictly
      // after the last-viewed instant.
      if (!timestamp) {
        state.rooms[chatJID].unreadMessages = 0;
      } else {
        state.rooms[chatJID].unreadMessages = countNewerMessages(state.rooms[chatJID].messages, timestamp);
      }
    }
  },
  setRoomRole: (state, action) => {
    const {
      chatJID,
      role
    } = action.payload;
    if (state.rooms[chatJID]) {
      state.rooms[chatJID].role = role;
    }
  },
  setRoomNoMessages: (state, action) => {
    const {
      value,
      chatJID
    } = action.payload;
    if (chatJID) {
      state.rooms[chatJID].noMessages = value;
    }
  },
  setCurrentRoom: (state, action) => {
    // Accept null/empty so callers can clear the active room (e.g.
    // back button from chat → return to RoomList). Mirrors web.
    state.activeRoomJID = action.payload.roomJID || '';
  },
  /**
   * Stash a JID that a push notification asked us to open before
   * the rooms list has loaded. The Chat component clears this once
   * the room exists locally and dispatches `setCurrentRoom`.
   */
  setPendingNotificationJid: (state, action) => {
    state.pendingNotificationJid = action.payload;
  },
  clearPendingNotificationJid: state => {
    state.pendingNotificationJid = null;
  },
  /**
   * Stamp a message in `state.rooms[roomJID].messages` with an updated
   * reactions list. The reactionsMiddleware listens for this action to
   * keep `IRoom.lastMessage` / `lastMessageTimestamp` in sync.
   */
  setReactions: (state, action) => {
    const {
      roomJID,
      messageId,
      reactions
    } = action.payload;
    const room = state.rooms[roomJID];
    if (!room?.messages) {
      return;
    }
    for (const msg of room.messages) {
      if (msg?.id === messageId) {
        msg.reactions = reactions;
        break;
      }
    }
  },
  setLogoutState: state => {
    state.rooms = {};
    state.activeRoomJID = null;
    state.isLoading = false;
  },
  setActiveMessage: (state, action) => {
    const {
      id,
      chatJID
    } = action.payload;
    state.rooms[chatJID].messages.map(message => {
      if (message.id === id) {
        message.activeMessage = true;
      } else {
        message.activeMessage = false;
      }
    });
  },
  setCloseActiveMessage: (state, action) => {
    const {
      chatJID
    } = action.payload;
    state.rooms[chatJID].messages.map(message => {
      message.activeMessage = false;
    });
  },
  /**
   * Batched update used by the history preload scheduler. Each patch is
   * applied to its room: messages REPLACE (when provided), state fields
   * MERGE. Rooms that don't exist yet are ignored.
   */
  addRoomFromApi: (state, action) => {
    const {
      room
    } = action.payload;
    if (!isValidRoomJid(room?.jid)) return;
    const existing = state.rooms[room.jid];
    const incomingMessages = Array.isArray(room.messages) ? room.messages : [];
    const existingMessages = Array.isArray(existing?.messages) ? existing.messages : [];
    state.rooms[room.jid] = {
      ...existing,
      ...room,
      title: room.title || existing?.title || room.title,
      usersCnt: (() => {
        const incoming = typeof room.usersCnt === 'number' && room.usersCnt > 0 ? room.usersCnt : 0;
        const previous = typeof existing?.usersCnt === 'number' && existing.usersCnt > 0 ? existing.usersCnt : 0;
        if (incoming === 0 && previous === 0) return room.usersCnt;
        return Math.max(incoming, previous);
      })(),
      icon: room.icon ?? existing?.icon,
      messages: existingMessages.length > 0 ? existingMessages : incomingMessages,
      unreadMessages: existing?.unreadMessages ?? room.unreadMessages ?? 0,
      lastViewedTimestamp: existing?.lastViewedTimestamp ?? room.lastViewedTimestamp ?? 0,
      unreadBaselineTimestamp: existing?.unreadBaselineTimestamp ?? existing?.lastViewedTimestamp ?? room.unreadBaselineTimestamp ?? room.lastViewedTimestamp ?? 0,
      composingList: existing?.composingList ?? room.composingList,
      composing: existing?.composing ?? room.composing,
      unreadCapped: existing?.unreadCapped ?? room.unreadCapped ?? false,
      historyPreloadState: existing?.historyPreloadState ?? room.historyPreloadState ?? 'idle',
      messageStats: existing?.messageStats ?? room.messageStats,
      historyComplete: existing?.historyComplete ?? room.historyComplete
    };
  },
  applyRoomsPreloadBatch: (state, action) => {
    const {
      rooms
    } = action.payload;
    for (const patch of rooms) {
      const room = state.rooms[patch.jid];
      if (!room) {
        continue;
      }
      if (typeof patch.historyPreloadState !== 'undefined') {
        room.historyPreloadState = patch.historyPreloadState;
      }
      if (typeof patch.unreadCapped !== 'undefined') {
        room.unreadCapped = patch.unreadCapped;
      }
      if (typeof patch.historyComplete !== 'undefined') {
        room.historyComplete = patch.historyComplete;
      }
      if (Array.isArray(patch.messages)) {
        // Merge: keep any locally-pending messages, replace the rest.
        const pending = room.messages?.filter(m => m?.pending) || [];
        room.messages = [...patch.messages, ...pending];
      }
    }
  }
};
const roomsStore = exports.roomsStore = (0, _toolkit.createSlice)({
  name: 'roomMessages',
  initialState,
  reducers
});

// Count messages strictly newer than the given millisecond timestamp.
// Uses `msg.id` (server-authoritative microsecond timestamp prefixed by
// 13-digit millis — see helpers/dateComparison `getHighResolutionTimestamp`)
// because `msg.date` can be derived client-side (createMessageFromXml
// falls back to `Date.now()` for realtime stanzas without a `date`
// attr), which makes the comparison drift vs what the server assigned.
// Excludes the "delimiter-new" sentinel, pending sends, and the current
// user's own messages (parity with unreadMiddleware's isOwnMessage
// filter — without this, the reducer and the middleware disagree about
// the count and we get a flicker as the badge gets written twice with
// different values on every message).
const msgSortableMs = msg => {
  const id = String(msg?.id || '');
  const m = /^(\d{13})/.exec(id);
  if (m) {
    return Number(m[1]);
  }
  if (msg?.date) {
    const t = new Date(msg.date).getTime();
    if (Number.isFinite(t)) {
      return t;
    }
  }
  return 0;
};
exports.msgSortableMs = msgSortableMs;
const norm = s => {
  if (s == null) {
    return '';
  }
  let v = String(s).toLowerCase();
  v = v.split('/')[0];
  v = v.split('@')[0];
  return v.replace(/_/g, '');
};
const isOwn = (msg, selfXmpp, selfWallet) => {
  if (!selfXmpp && !selfWallet) {
    return false;
  }
  const candidates = [norm(msg?.user?.id), norm(msg?.user?.userJID), norm(msg?.user?.xmppUsername), norm(msg?.xmppFrom)].filter(Boolean);
  if (candidates.length === 0) {
    return false;
  }
  const self = new Set([norm(selfXmpp), norm(selfWallet)].filter(Boolean));
  for (const c of candidates) {
    if (self.has(c)) {
      return true;
    }
  }
  return false;
};
const countNewerMessages = (messages, timestamp, selfXmpp = '', selfWallet = '') => {
  if (!messages?.length || !timestamp) {
    return 0;
  }
  let count = 0;
  for (const message of messages) {
    if (!message || message.id === 'delimiter-new' || message.pending) {
      continue;
    }
    if (isOwn(message, selfXmpp, selfWallet)) {
      continue;
    }
    const ms = msgSortableMs(message);
    if (Number.isFinite(ms) && ms > timestamp) {
      count += 1;
    }
  }
  return count;
};
const {
  addRoom,
  deleteAllRooms,
  setRoomMessages,
  addRoomMessage,
  deleteRoomMessage,
  setEditAction,
  editRoomMessage,
  setComposing,
  setIsLoading,
  setLastViewedTimestamp,
  setRoomNoMessages,
  setCurrentRoom,
  setRoomRole,
  setLogoutState,
  setActiveMessage,
  setCloseActiveMessage,
  deleteRoom,
  updateRoom,
  applyRoomsPreloadBatch,
  setPendingNotificationJid,
  clearPendingNotificationJid,
  setReactions
} = roomsStore.actions;
exports.setReactions = setReactions;
exports.clearPendingNotificationJid = clearPendingNotificationJid;
exports.setPendingNotificationJid = setPendingNotificationJid;
exports.applyRoomsPreloadBatch = applyRoomsPreloadBatch;
exports.updateRoom = updateRoom;
exports.deleteRoom = deleteRoom;
exports.setCloseActiveMessage = setCloseActiveMessage;
exports.setActiveMessage = setActiveMessage;
exports.setLogoutState = setLogoutState;
exports.setRoomRole = setRoomRole;
exports.setCurrentRoom = setCurrentRoom;
exports.setRoomNoMessages = setRoomNoMessages;
exports.setLastViewedTimestamp = setLastViewedTimestamp;
exports.setIsLoading = setIsLoading;
exports.setComposing = setComposing;
exports.editRoomMessage = editRoomMessage;
exports.setEditAction = setEditAction;
exports.deleteRoomMessage = deleteRoomMessage;
exports.addRoomMessage = addRoomMessage;
exports.setRoomMessages = setRoomMessages;
exports.deleteAllRooms = deleteAllRooms;
exports.addRoom = addRoom;
var _default = exports.default = roomsStore.reducer;
//# sourceMappingURL=roomsSlice.js.map