import { useAtomValue } from 'jotai';
import { MatrixEvent, RoomStateEvent } from 'matrix-js-sdk';
import { useEffect, useMemo, useState } from 'react';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { mDirectAtom } from '../../../state/mDirectList';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useDirects, useOrphanRooms } from '../../../state/hooks/roomList';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useBotPresets } from '../../../features/bots/catalog';
import { isBridgeStateEvent, isCatalogBotControlRoom } from '../../../features/bots/room';
import { isBridgedRoom } from '../../../utils/room';

// After P3c the Direct tab is universal: every joined non-space «orphan»
// room renders here, regardless of `m.direct`, except curated bot control DMs
// (Robots tab) and bridged portal rooms (mautrix-telegram/whatsapp/discord/…).
// Bridged rooms are filtered out here so they live exclusively in their
// per-bridge personal filtering space (Channels tab) — otherwise a 1:1
// Telegram chat that the bridge tagged `m.direct` AND placed inside the
// Telegram space would appear twice. Implementation =
// `useOrphanRooms ∪ useDirects`, minus catalog bot control rooms and bridged
// portals:
//
//   - `useOrphanRooms` = `isRoom && !mDirects.has && !roomToParents.has` →
//     non-space rooms that don't live inside any space and aren't m.direct-
//     tagged. The formerly-Home tab.
//   - `useDirects` = `isRoom && mDirects.has` → every m.direct-tagged
//     non-space room. **No `roomToParents` filter** — a room that is BOTH a
//     space child AND m.direct-tagged appears here. This is intentional and
//     pre-dates P3c: the m.direct flag wins over the space-child membership
//     for routing purposes (user sees the DM in /direct/, opening it loses
//     space context). The same room is hidden from the parent space's child-
//     room navigation list (`useChildRoomScopeFactory` excludes m.direct);
//     it stays reachable from space message search via
//     `useRecursiveChildNonSpaceRoomScopeFactory` (post-P3c universal).
//
// Duplicates inside the union are deduped via Set semantics below. The
// product call (DM-tagged-space-child resolves to /direct/, not /space/{id}/)
// is open for revision — see desired_features §21 (Channels plan).
export const useDirectRooms = (): string[] => {
  const mx = useMatrixClient();
  const mDirects = useAtomValue(mDirectAtom);
  const roomToParents = useAtomValue(roomToParentsAtom);
  const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents);
  const directs = useDirects(mx, allRoomsAtom, mDirects);
  const bots = useBotPresets();
  const directCandidates = useMemo(
    () => new Set([...orphanRooms, ...directs]),
    [orphanRooms, directs]
  );
  const [roomStateTick, setRoomStateTick] = useState(0);

  useEffect(() => {
    const bumpIfCandidate = (roomId: string | undefined) => {
      if (roomId && directCandidates.has(roomId)) {
        setRoomStateTick((tick) => tick + 1);
      }
    };
    const onMembers = (ev: MatrixEvent) => {
      bumpIfCandidate(ev.getRoomId());
    };
    const onStateEvent = (ev: MatrixEvent) => {
      if (isBridgeStateEvent(ev)) bumpIfCandidate(ev.getRoomId());
    };

    mx.on(RoomStateEvent.Members, onMembers);
    mx.on(RoomStateEvent.Events, onStateEvent);

    return () => {
      mx.removeListener(RoomStateEvent.Members, onMembers);
      mx.removeListener(RoomStateEvent.Events, onStateEvent);
    };
  }, [mx, directCandidates]);

  // The output MUST be a stable reference across renders that don't change
  // input identity — `useRoomsUnread` (state/hooks/unread.ts:37) feeds this
  // array into a `useCallback([rooms])` whose result is then handed to
  // `selectAtom(...)` on every render. If `rooms` identity flips each render,
  // `selectAtom` produces a fresh atom subscription which triggers another
  // render — that's the React-detected "Maximum update depth exceeded" loop
  // observed in DirectTab. `roomStateTick` is in deps so member/bridge state
  // updates still recompute the visible set.
  return useMemo(() => {
    const seen = new Set<string>();
    const out: string[] = [];
    const isVisibleDirect = (id: string): boolean => {
      const room = mx.getRoom(id);
      if (!room) return true;
      if (isCatalogBotControlRoom(mx, room, bots)) return false;
      if (isBridgedRoom(room)) return false;
      return true;
    };

    orphanRooms.forEach((id) => {
      if (!seen.has(id) && isVisibleDirect(id)) {
        seen.add(id);
        out.push(id);
      }
    });
    directs.forEach((id) => {
      if (!seen.has(id) && isVisibleDirect(id)) {
        seen.add(id);
        out.push(id);
      }
    });

    return out;
    // roomStateTick is intentional: the listener bumps it on member/bridge
    // state events to force re-evaluation, even though the value isn't read
    // inside the body. Removing it would make the visible-direct set go
    // stale until one of the input arrays' identities changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mx, orphanRooms, directs, bots, roomStateTick]);
};
