import type { QueryChannelsResponseWithChannels, StreamChat } from './client';
import type {
  ChannelFilters,
  ChannelOptions,
  ChannelSort,
  ChannelStateOptions,
  Event,
  QueryChannelsAPIResponse,
} from './types';
import type { ValueOrPatch } from './store';
import { isPatch, StateStore } from './store';
import type { Channel } from './channel';
import {
  extractSortValue,
  findLastPinnedChannelIndex,
  getAndWatchChannel,
  isChannelArchived,
  isChannelPinned,
  promoteChannel,
  shouldConsiderArchivedChannels,
  shouldConsiderPinnedChannels,
  sleep,
  uniqBy,
} from './utils';
import { generateUUIDv4 } from './utils';
import {
  DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES,
  DEFAULT_QUERY_CHANNELS_RETRY_COUNT,
} from './constants';
import { WithSubscriptions } from './utils/WithSubscriptions';

export type ChannelManagerPagination = {
  filters: ChannelFilters;
  hasNext: boolean;
  isLoading: boolean;
  isLoadingNext: boolean;
  options: ChannelOptions;
  responseFilters?: ChannelFilters;
  responseSort?: ChannelSort;
  sort: ChannelSort;
};

export type ChannelManagerState = {
  channels: Channel[];
  /**
   * This value will become true the first time queryChannels is successfully executed and
   * will remain false otherwise. It's used as a control property regarding whether the list
   * has been initialized yet (i.e a query has already been done at least once) or not. We do
   * this to prevent state.channels from being forced to be nullable.
   */
  initialized: boolean;
  pagination: ChannelManagerPagination;
  error: Error | undefined;
};

export type ChannelSetterParameterType = ValueOrPatch<ChannelManagerState['channels']>;
export type ChannelSetterType = (arg: ChannelSetterParameterType) => void;

export type GenericEventHandlerType<T extends unknown[]> = (
  ...args: T
) => void | (() => void) | ((...args: T) => Promise<void>) | Promise<void>;
export type EventHandlerType = GenericEventHandlerType<[Event]>;
export type EventHandlerOverrideType = GenericEventHandlerType<
  [ChannelSetterType, Event]
>;

export type ChannelManagerEventTypes =
  | 'notification.added_to_channel'
  | 'notification.message_new'
  | 'notification.removed_from_channel'
  | 'message.new'
  | 'member.updated'
  | 'channel.deleted'
  | 'channel.hidden'
  | 'channel.truncated'
  | 'channel.visible'
  | 'channel.updated';

export type ChannelManagerEventHandlerNames =
  | 'channelDeletedHandler'
  | 'channelHiddenHandler'
  | 'channelTruncatedHandler'
  | 'channelUpdatedHandler'
  | 'channelVisibleHandler'
  | 'newMessageHandler'
  | 'memberUpdatedHandler'
  | 'notificationAddedToChannelHandler'
  | 'notificationNewMessageHandler'
  | 'notificationRemovedFromChannelHandler';

export type ChannelManagerEventHandlerOverrides = Partial<
  Record<ChannelManagerEventHandlerNames, EventHandlerOverrideType>
>;

export type ExecuteChannelsQueryPayload = Pick<
  ChannelManagerPagination,
  'filters' | 'sort' | 'options'
> & { stateOptions: ChannelStateOptions };

export const channelManagerEventToHandlerMapping: {
  [key in ChannelManagerEventTypes]: ChannelManagerEventHandlerNames;
} = {
  'channel.deleted': 'channelDeletedHandler',
  'channel.hidden': 'channelHiddenHandler',
  'channel.truncated': 'channelTruncatedHandler',
  'channel.updated': 'channelUpdatedHandler',
  'channel.visible': 'channelVisibleHandler',
  'message.new': 'newMessageHandler',
  'member.updated': 'memberUpdatedHandler',
  'notification.added_to_channel': 'notificationAddedToChannelHandler',
  'notification.message_new': 'notificationNewMessageHandler',
  'notification.removed_from_channel': 'notificationRemovedFromChannelHandler',
};

export type ChannelManagerOptions = {
  /**
   * Aborts a channels query that is already in progress and runs the new one.
   */
  abortInFlightQuery?: boolean;
  /**
   * Allows channel promotion to be applied where applicable for channels that are
   * currently not part of the channel list within the state. A good example of
   * this would be a channel that is being watched and it receives a new message,
   * but is not part of the list initially.
   */
  allowNotLoadedChannelPromotionForEvent?: {
    'channel.visible': boolean;
    'message.new': boolean;
    'notification.added_to_channel': boolean;
    'notification.message_new': boolean;
  };
  /**
   * Allows us to lock the order of channels within the list. Any event that would
   * change the order of channels within the list will do nothing.
   */
  lockChannelOrder?: boolean;
};

export type QueryChannelsRequestOutput = Channel[] | QueryChannelsResponseWithChannels;

export type QueryChannelsRequestType = (
  filters: ChannelFilters,
  sort?: ChannelSort,
  options?: ChannelOptions,
  stateOptions?: ChannelStateOptions,
) => Promise<QueryChannelsRequestOutput>;

export const DEFAULT_CHANNEL_MANAGER_OPTIONS = {
  abortInFlightQuery: false,
  allowNotLoadedChannelPromotionForEvent: {
    'channel.visible': true,
    'message.new': true,
    'notification.added_to_channel': true,
    'notification.message_new': true,
  },
  lockChannelOrder: false,
};

export const DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS = {
  offset: 0,
};

const mapPredefinedFilterSortToChannelSort = (
  sort: NonNullable<QueryChannelsAPIResponse['predefined_filter']>['sort'],
): ChannelSort =>
  (sort ?? []).map(({ direction = 1, field }) => ({
    [field]: direction,
  })) as ChannelSort;

const getResponsePaginationParams = ({
  queryChannelsResponse,
  sort,
}: {
  queryChannelsResponse?: Pick<QueryChannelsAPIResponse, 'predefined_filter'>;
  sort: ChannelSort;
}): Pick<ChannelManagerPagination, 'responseFilters' | 'responseSort'> => {
  const predefinedFilter = queryChannelsResponse?.predefined_filter;

  if (!predefinedFilter) {
    return {};
  }

  return {
    responseFilters: predefinedFilter.filter as ChannelFilters,
    responseSort:
      predefinedFilter.sort !== undefined
        ? mapPredefinedFilterSortToChannelSort(predefinedFilter.sort)
        : sort,
  };
};

const getResponseFiltersAndSort = (
  pagination: ChannelManagerPagination,
): Pick<ChannelManagerPagination, 'filters' | 'sort'> => ({
  filters: pagination.responseFilters ?? pagination.filters,
  sort: pagination.responseSort ?? pagination.sort,
});

const omitResponsePaginationParams = (pagination: ChannelManagerPagination) => {
  const paginationWithoutResponseParams = { ...pagination };
  delete paginationWithoutResponseParams.responseFilters;
  delete paginationWithoutResponseParams.responseSort;

  return paginationWithoutResponseParams;
};

const isQueryChannelsResponseWithChannels = (
  response: QueryChannelsRequestOutput,
): response is QueryChannelsResponseWithChannels => !Array.isArray(response);

/**
 * A class that manages a list of channels and changes it based on configuration and WS events. The
 * list of channels is reactive as well as the pagination and it can be subscribed to for state updates.
 *
 * @internal
 */
export class ChannelManager extends WithSubscriptions {
  public readonly state: StateStore<ChannelManagerState>;
  private client: StreamChat;
  private eventHandlers: Map<string, EventHandlerType> = new Map();
  private eventHandlerOverrides: Map<string, EventHandlerOverrideType> = new Map();
  private queryChannelsRequest: QueryChannelsRequestType;
  private options: ChannelManagerOptions = {};
  private stateOptions: ChannelStateOptions = {};
  private id: string;

  constructor({
    client,
    eventHandlerOverrides = {},
    options = {},
    queryChannelsOverride,
  }: {
    client: StreamChat;
    eventHandlerOverrides?: ChannelManagerEventHandlerOverrides;
    options?: ChannelManagerOptions;
    queryChannelsOverride?: QueryChannelsRequestType;
  }) {
    super();

    this.id = `channel-manager-${generateUUIDv4()}`;
    this.client = client;
    this.state = new StateStore<ChannelManagerState>({
      channels: [],
      pagination: {
        isLoading: false,
        isLoadingNext: false,
        hasNext: false,
        filters: {},
        sort: {},
        options: DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
      },
      initialized: false,
      error: undefined,
    });
    this.setEventHandlerOverrides(eventHandlerOverrides);
    this.setOptions(options);
    this.queryChannelsRequest =
      queryChannelsOverride ?? ((...params) => this.client.queryChannels(...params));
    this.eventHandlers = new Map(
      Object.entries<EventHandlerType>({
        channelDeletedHandler: this.channelDeletedHandler,
        channelHiddenHandler: this.channelHiddenHandler,
        channelVisibleHandler: this.channelVisibleHandler,
        memberUpdatedHandler: this.memberUpdatedHandler,
        newMessageHandler: this.newMessageHandler,
        notificationAddedToChannelHandler: this.notificationAddedToChannelHandler,
        notificationNewMessageHandler: this.notificationNewMessageHandler,
        notificationRemovedFromChannelHandler: this.notificationRemovedFromChannelHandler,
      }),
    );
  }

  public setChannels = (valueOrFactory: ChannelSetterParameterType) => {
    this.state.next((current) => {
      const { channels: currentChannels } = current;
      const newChannels = isPatch(valueOrFactory)
        ? valueOrFactory(currentChannels)
        : valueOrFactory;

      // If the references between the two values are the same, just return the
      // current state; otherwise trigger a state change.
      if (currentChannels === newChannels) {
        return current;
      }

      return { ...current, channels: newChannels };
    });
    const {
      channels,
      pagination: { filters, options, sort },
    } = this.state.getLatestValue();
    this.client.offlineDb?.executeQuerySafely(
      (db) =>
        db.upsertCidsForQuery({
          cids: channels.map((channel) => channel.cid),
          filters,
          options,
          sort,
        }),
      { method: 'upsertCidsForQuery' },
    );
  };

  public setEventHandlerOverrides = (
    eventHandlerOverrides: ChannelManagerEventHandlerOverrides = {},
  ) => {
    const truthyEventHandlerOverrides = Object.entries(eventHandlerOverrides).reduce<
      Partial<ChannelManagerEventHandlerOverrides>
    >((acc, [key, value]) => {
      if (value) {
        acc[key as keyof ChannelManagerEventHandlerOverrides] = value;
      }
      return acc;
    }, {});
    this.eventHandlerOverrides = new Map(
      Object.entries<EventHandlerOverrideType>(truthyEventHandlerOverrides),
    );
  };

  public setQueryChannelsRequest = (queryChannelsRequest: QueryChannelsRequestType) => {
    this.queryChannelsRequest = queryChannelsRequest;
  };

  public setOptions = (options: ChannelManagerOptions = {}) => {
    this.options = { ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options };
  };

  private executeChannelsQuery = async (
    payload: ExecuteChannelsQueryPayload,
    retryCount = 0,
  ): Promise<void> => {
    const { filters, sort, options, stateOptions } = payload;
    const { offset, limit } = {
      ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
      ...options,
    };
    try {
      const queryChannelsResponse = await this.queryChannelsRequest(
        filters,
        sort,
        options,
        { ...stateOptions, withResponse: true },
      );
      const channels = isQueryChannelsResponseWithChannels(queryChannelsResponse)
        ? queryChannelsResponse.channels
        : queryChannelsResponse;
      const newOffset = offset + (channels?.length ?? 0);
      const newOptions = { ...options, offset: newOffset };
      const { pagination } = this.state.getLatestValue();
      const responsePaginationParams = getResponsePaginationParams({
        queryChannelsResponse: isQueryChannelsResponseWithChannels(queryChannelsResponse)
          ? queryChannelsResponse
          : undefined,
        sort,
      });
      const paginationWithoutResponseParams = omitResponsePaginationParams(pagination);

      this.state.partialNext({
        channels,
        pagination: {
          // Drop response derived filter/sort from the previous query before applying
          // the current response. Non predefined queries do not return this metadata,
          // so keeping the old values would make later WS mutations use stale
          // predefined filter semantics. Also the predefined_filter might change, producing
          // a different combination as well so we always need to first clean up.
          ...paginationWithoutResponseParams,
          hasNext: (channels?.length ?? 0) >= (limit ?? 1),
          isLoading: false,
          options: newOptions,
          ...responsePaginationParams,
        },
        initialized: true,
        error: undefined,
      });
      this.client.offlineDb?.executeQuerySafely(
        (db) =>
          db.upsertCidsForQuery({
            cids: channels.map((channel) => channel.cid),
            filters: pagination.filters,
            options,
            sort: pagination.sort,
          }),
        { method: 'upsertCidsForQuery' },
      );
    } catch (err) {
      if (retryCount >= DEFAULT_QUERY_CHANNELS_RETRY_COUNT) {
        console.warn(err);

        const wrappedError = new Error(
          `Maximum number of retries reached in queryChannels. Last error message is: ${err}`,
        );

        const state = this.state.getLatestValue();
        // If the offline support is enabled, and there are channels in the DB, we should not error out.
        const isOfflineSupportEnabledWithChannels =
          this.client.offlineDb && state.channels.length > 0;

        this.state.partialNext({
          error: isOfflineSupportEnabledWithChannels ? undefined : wrappedError,
          pagination: {
            ...state.pagination,
            isLoading: false,
            isLoadingNext: false,
          },
        });
        return;
      }

      await sleep(DEFAULT_QUERY_CHANNELS_MS_BETWEEN_RETRIES);

      return this.executeChannelsQuery(payload, retryCount + 1);
    }
  };

  public queryChannels = async (
    filters: ChannelFilters,
    sort: ChannelSort = [],
    options: ChannelOptions = {},
    stateOptions: ChannelStateOptions = {},
  ) => {
    const {
      pagination: { isLoading, filters: filtersFromState },
      initialized,
    } = this.state.getLatestValue();

    if (
      isLoading &&
      !this.options.abortInFlightQuery &&
      // TODO: Figure a proper way to either deeply compare these or
      //       create hashes from each.
      JSON.stringify(filtersFromState) === JSON.stringify(filters)
    ) {
      return;
    }

    const executeChannelsQueryPayload = { filters, sort, options, stateOptions };

    try {
      this.stateOptions = stateOptions;
      this.state.next((currentState) => ({
        ...currentState,
        pagination: {
          ...omitResponsePaginationParams(currentState.pagination),
          isLoading: true,
          isLoadingNext: false,
          filters,
          sort,
          options,
        },
        error: undefined,
      }));

      if (this.client.offlineDb?.getChannelsForQuery && this.client.user?.id) {
        if (!initialized) {
          const channelsFromDB = await this.client.offlineDb.getChannelsForQuery({
            userId: this.client.user.id,
            filters,
            options,
            sort,
          });

          if (channelsFromDB) {
            const offlineChannels = this.client.hydrateActiveChannels(channelsFromDB, {
              offlineMode: true,
              skipInitialization: [], // passing empty array will clear out the existing messages from channel state, this removes the possibility of duplicate messages
            });

            this.state.partialNext({ channels: offlineChannels });
          }
        }

        if (!this.client.offlineDb.syncManager.syncStatus) {
          this.client.offlineDb.syncManager.scheduleSyncStatusChangeCallback(
            this.id,
            async () => {
              await this.executeChannelsQuery(executeChannelsQueryPayload);
            },
          );
          return;
        }
      }
      await this.executeChannelsQuery(executeChannelsQueryPayload);
    } catch (error) {
      this.client.logger('error', (error as Error).message);
      this.state.next((currentState) => ({
        ...currentState,
        pagination: { ...currentState.pagination, isLoading: false },
      }));
      throw error;
    }
  };

  public loadNext = async () => {
    const { pagination, initialized } = this.state.getLatestValue();
    const { filters, sort, options, isLoadingNext, hasNext } = pagination;

    if (!initialized || isLoadingNext || !hasNext) {
      return;
    }

    try {
      const { offset, limit } = {
        ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
        ...options,
      };
      this.state.partialNext({
        pagination: { ...pagination, isLoading: false, isLoadingNext: true },
      });
      const queryChannelsResponse = await this.queryChannelsRequest(
        filters,
        sort,
        options,
        this.stateOptions,
      );
      const nextChannels = isQueryChannelsResponseWithChannels(queryChannelsResponse)
        ? queryChannelsResponse.channels
        : queryChannelsResponse;
      const { channels } = this.state.getLatestValue();
      const newOffset = offset + (nextChannels?.length ?? 0);
      const newOptions = { ...options, offset: newOffset };

      this.state.partialNext({
        channels: uniqBy<Channel>([...(channels || []), ...nextChannels], 'cid'),
        pagination: {
          ...pagination,
          hasNext: (nextChannels?.length ?? 0) >= (limit ?? 1),
          isLoading: false,
          isLoadingNext: false,
          options: newOptions,
        },
      });
    } catch (error) {
      this.client.logger('error', (error as Error).message);
      this.state.next((currentState) => ({
        ...currentState,
        pagination: {
          ...currentState.pagination,
          isLoadingNext: false,
          isLoading: false,
        },
      }));
      throw error;
    }
  };

  private notificationAddedToChannelHandler = async (event: Event) => {
    const { id, type, members } = event?.channel ?? {};

    if (
      !type ||
      !this.options.allowNotLoadedChannelPromotionForEvent?.[
        'notification.added_to_channel'
      ]
    ) {
      return;
    }

    const channel = await getAndWatchChannel({
      client: this.client,
      id,
      members: members?.reduce<string[]>((acc, { user, user_id }) => {
        const userId = user_id || user?.id;
        if (userId) {
          acc.push(userId);
        }
        return acc;
      }, []),
      type,
    });

    const { pagination, channels } = this.state.getLatestValue();
    if (!channels) {
      return;
    }

    const { sort } = getResponseFiltersAndSort(pagination);

    this.setChannels(
      promoteChannel({
        channels,
        channelToMove: channel,
        sort,
      }),
    );
  };

  private channelDeletedHandler = (event: Event) => {
    const { channels } = this.state.getLatestValue();
    if (!channels) {
      return;
    }

    const newChannels = [...channels];
    const channelIndex = newChannels.findIndex(
      (channel) => channel.cid === (event.cid || event.channel?.cid),
    );

    if (channelIndex < 0) {
      return;
    }

    newChannels.splice(channelIndex, 1);
    this.setChannels(newChannels);
  };

  private channelHiddenHandler = this.channelDeletedHandler;

  private newMessageHandler = (event: Event) => {
    const { pagination, channels } = this.state.getLatestValue();
    if (!channels) {
      return;
    }
    const { filters, sort } = getResponseFiltersAndSort(pagination);

    const channelType = event.channel_type;
    const channelId = event.channel_id;

    if (!channelType || !channelId) {
      return;
    }

    const targetChannel = this.client.channel(channelType, channelId);
    const targetChannelIndex = channels.indexOf(targetChannel);
    const targetChannelExistsWithinList = targetChannelIndex >= 0;

    const isTargetChannelPinned = isChannelPinned(targetChannel);
    const isTargetChannelArchived = isChannelArchived(targetChannel);

    const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
    const considerPinnedChannels = shouldConsiderPinnedChannels(sort);

    if (
      // filter is defined, target channel is archived and filter option is set to false
      (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
      // filter is defined, target channel isn't archived and filter option is set to true
      (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
      // sort option is defined, target channel is pinned
      (considerPinnedChannels && isTargetChannelPinned) ||
      // list order is locked
      this.options.lockChannelOrder ||
      // target channel is not within the loaded list and loading from cache is disallowed
      (!targetChannelExistsWithinList &&
        !this.options.allowNotLoadedChannelPromotionForEvent?.['message.new'])
    ) {
      return;
    }

    this.setChannels(
      promoteChannel({
        channels,
        channelToMove: targetChannel,
        channelToMoveIndexWithinChannels: targetChannelIndex,
        sort,
      }),
    );
  };

  private notificationNewMessageHandler = async (event: Event) => {
    const { id, type } = event?.channel ?? {};

    if (!id || !type) {
      return;
    }

    const channel = await getAndWatchChannel({
      client: this.client,
      id,
      type,
    });

    const { channels, pagination } = this.state.getLatestValue();
    const { filters, sort } = getResponseFiltersAndSort(pagination);

    const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
    const isTargetChannelArchived = isChannelArchived(channel);

    if (
      !channels ||
      (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
      (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
      !this.options.allowNotLoadedChannelPromotionForEvent?.['notification.message_new']
    ) {
      return;
    }

    this.setChannels(
      promoteChannel({
        channels,
        channelToMove: channel,
        sort,
      }),
    );
  };

  private channelVisibleHandler = async (event: Event) => {
    const { channel_type: channelType, channel_id: channelId } = event;

    if (!channelType || !channelId) {
      return;
    }

    const channel = await getAndWatchChannel({
      client: this.client,
      id: event.channel_id,
      type: event.channel_type,
    });

    const { channels, pagination } = this.state.getLatestValue();
    const { filters, sort } = getResponseFiltersAndSort(pagination);

    const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
    const isTargetChannelArchived = isChannelArchived(channel);

    if (
      !channels ||
      (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
      (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
      !this.options.allowNotLoadedChannelPromotionForEvent?.['channel.visible']
    ) {
      return;
    }

    this.setChannels(
      promoteChannel({
        channels,
        channelToMove: channel,
        sort,
      }),
    );
  };

  private notificationRemovedFromChannelHandler = this.channelDeletedHandler;

  private memberUpdatedHandler = (event: Event) => {
    const { pagination, channels } = this.state.getLatestValue();
    const { filters, sort } = getResponseFiltersAndSort(pagination);
    if (
      !event.member?.user ||
      event.member.user.id !== this.client.userID ||
      !event.channel_type ||
      !event.channel_id
    ) {
      return;
    }
    const channelType = event.channel_type;
    const channelId = event.channel_id;

    const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
    const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
    const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });

    if (
      !channels ||
      (!considerPinnedChannels && !considerArchivedChannels) ||
      this.options.lockChannelOrder
    ) {
      return;
    }

    const targetChannel = this.client.channel(channelType, channelId);
    // assumes that channel instances are not changing
    const targetChannelIndex = channels.indexOf(targetChannel);
    const targetChannelExistsWithinList = targetChannelIndex >= 0;

    const isTargetChannelPinned = isChannelPinned(targetChannel);
    const isTargetChannelArchived = isChannelArchived(targetChannel);

    const newChannels = [...channels];

    if (targetChannelExistsWithinList) {
      newChannels.splice(targetChannelIndex, 1);
    }

    // handle archiving (remove channel)
    if (
      // When archived filter true, and channel is unarchived
      (considerArchivedChannels && !isTargetChannelArchived && filters?.archived) ||
      // When archived filter false, and channel is archived
      (considerArchivedChannels && isTargetChannelArchived && !filters?.archived)
    ) {
      this.setChannels(newChannels);
      return;
    }

    // handle pinning
    let lastPinnedChannelIndex: number | null = null;

    if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !isTargetChannelPinned)) {
      lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
    }
    const newTargetChannelIndex =
      typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0;

    // skip state update if the position of the channel does not change
    if (channels[newTargetChannelIndex] === targetChannel) {
      return;
    }

    newChannels.splice(newTargetChannelIndex, 0, targetChannel);
    this.setChannels(newChannels);
  };

  private subscriptionOrOverride = (event: Event) => {
    const handlerName =
      channelManagerEventToHandlerMapping[event.type as ChannelManagerEventTypes];
    const defaultEventHandler = this.eventHandlers.get(handlerName);
    const eventHandlerOverride = this.eventHandlerOverrides.get(handlerName);
    if (eventHandlerOverride && typeof eventHandlerOverride === 'function') {
      eventHandlerOverride(this.setChannels, event);
      return;
    }

    if (defaultEventHandler && typeof defaultEventHandler === 'function') {
      defaultEventHandler(event);
    }
  };

  public registerSubscriptions = () => {
    if (this.hasSubscriptions) {
      // Already listening for events and changes
      return;
    }

    for (const eventType of Object.keys(channelManagerEventToHandlerMapping)) {
      this.addUnsubscribeFunction(
        this.client.on(eventType, this.subscriptionOrOverride).unsubscribe,
      );
    }
  };
}
