import { useEffect, useRef } from 'react';
import {
  ClientEvent,
  MatrixClient,
  Room,
  RoomEmittedEvents,
  RoomEventHandlerMap,
} from 'matrix-js-sdk';

/**
 * Attach `handler` for `event` on every joined/known room, including rooms
 * created after mount (via `ClientEvent.Room`). All listeners are detached on
 * unmount or when `mx`/`event` change.
 *
 * The handler is stored in a ref (mirroring `useTauriEvent`) so callers don't
 * need to memoize it — changing the handler identity never re-attaches the
 * per-room listeners.
 *
 * The emitting {@link Room} is PREPENDED as the first argument, before the
 * event's own args: several room-level SDK events (e.g.
 * `RoomEvent.UnreadNotifications`) don't include the room in their payload,
 * which callers need for per-room updates. Prepending (not appending) is
 * load-bearing — some SDK events emit with VARIABLE arity
 * (UnreadNotifications fires with 0, 1, or 2 args), so a trailing extra arg
 * would land in a different positional slot per emit.
 */
export function useRoomsListener<E extends RoomEmittedEvents>(
  mx: MatrixClient,
  event: E,
  handler: (room: Room, ...args: Parameters<RoomEventHandlerMap[E]>) => void,
): void {
  const handlerRef = useRef(handler);
  handlerRef.current = handler;

  useEffect(() => {
    // Track attached rooms (and their per-room trampolines) so re-emitted
    // `ClientEvent.Room` (e.g. on membership changes) never double-subscribes,
    // and cleanup can detach exactly what was attached.
    const attached = new Map<string, (...args: unknown[]) => void>();

    const attach = (room: Room) => {
      if (attached.has(room.roomId)) return;
      // Per-room trampoline: forwards to the current ref value with the
      // emitting room PREPENDED (stable slot regardless of emit arity).
      const roomHandler = (...args: unknown[]) =>
        (handlerRef.current as (...a: unknown[]) => void)(room, ...args);
      attached.set(room.roomId, roomHandler);
      // `event`/`roomHandler` are correlated through E but TS can't prove it
      // for the open generic, so we assert at the boundary.
      room.on(event, roomHandler as any);
    };

    mx.getRooms().forEach(attach);

    const handleRoom = (room: Room) => attach(room);
    mx.on(ClientEvent.Room, handleRoom);

    return () => {
      mx.removeListener(ClientEvent.Room, handleRoom);
      attached.forEach((roomHandler, roomId) => {
        mx.getRoom(roomId)?.removeListener(event, roomHandler as any);
      });
      attached.clear();
    };
  }, [mx, event]);
}
