import { useEffect } from 'react';
import { MatrixClient, RoomStateEvent, RoomStateEventHandlerMap } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';
import { useForceUpdate } from './useForceUpdate';

type MemberListener = () => void;

const memberKey = (roomId: string, userId: string): string => `${roomId}|${userId}`;

/**
 * Shared room-member-change store. Previously every ReadReceiptAvatars row and
 * every useMemberAvatar registered its own global `RoomStateEvent.Members`
 * listener — ~6 per receipt row — each firing on ANY membership / display-name /
 * avatar change in ANY room (PERF-3). This registers exactly ONE listener total
 * and fans out to subscribers keyed by `roomId|userId`.
 *
 * (`RoomStateEvent.Members` is used rather than `RoomMemberEvent` because the
 * latter has no dedicated avatar signal.)
 */
class MemberChangeStore {
  private mx: MatrixClient | undefined;

  private started = false;

  private subscribers = new Map<string, Set<MemberListener>>();

  private handleMembers: RoomStateEventHandlerMap[RoomStateEvent.Members] = (
    event,
    _state,
    member,
  ) => {
    const roomId = event.getRoomId();
    if (!roomId) return;
    const subs = this.subscribers.get(memberKey(roomId, member.userId));
    if (!subs) return;
    subs.forEach((cb) => cb());
  };

  private start(mx: MatrixClient): void {
    if (this.started && this.mx === mx) return;
    if (this.started && this.mx) {
      this.mx.removeListener(RoomStateEvent.Members, this.handleMembers);
    }
    this.mx = mx;
    mx.on(RoomStateEvent.Members, this.handleMembers);
    this.started = true;
  }

  subscribe(mx: MatrixClient, roomId: string, userId: string, cb: MemberListener): () => void {
    this.start(mx);
    const key = memberKey(roomId, userId);
    let subs = this.subscribers.get(key);
    if (!subs) {
      subs = new Set();
      this.subscribers.set(key, subs);
    }
    subs.add(cb);
    return () => {
      subs.delete(cb);
      // Only drop the registry entry if this is still the live set for the key,
      // keeping unsubscribe idempotent against a double-invoke / re-subscribe.
      if (subs.size === 0 && this.subscribers.get(key) === subs) {
        this.subscribers.delete(key);
      }
    };
  }
}

export const memberChangeStore = new MemberChangeStore();

/**
 * Re-render the caller whenever the given room member's state (display name,
 * avatar, or membership) changes, via the shared single-listener store.
 */
export const useRoomMemberChange = (roomId: string, userId: string): void => {
  const mx = useMatrixClient();
  const [, forceUpdate] = useForceUpdate();
  useEffect(
    () => memberChangeStore.subscribe(mx, roomId, userId, forceUpdate),
    [mx, roomId, userId, forceUpdate],
  );
};

/**
 * Multi-user variant: re-render when ANY of `userIds` changes. Subscribes to
 * each id through the shared store from a single effect (no per-id hook).
 */
export const useRoomMembersChange = (roomId: string, userIds: string[]): void => {
  const mx = useMatrixClient();
  const [, forceUpdate] = useForceUpdate();
  // Stable, order-independent dependency for the set of ids (the array identity
  // is unstable, and re-ordering the same members shouldn't churn subscriptions).
  const idsKey = [...userIds].sort().join(',');
  useEffect(() => {
    const unsubs = userIds.map((userId) =>
      memberChangeStore.subscribe(mx, roomId, userId, forceUpdate),
    );
    return () => unsubs.forEach((unsub) => unsub());
    // `userIds` is captured via the stable `idsKey`; re-subscribes when it changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mx, roomId, idsKey, forceUpdate]);
};
