import { useCallback, useEffect } from 'react';
import { useSetAtom } from 'jotai';
import {
  ClientEvent,
  IRoomTimelineData,
  MatrixClient,
  MatrixEvent,
  Room,
  RoomEvent,
  SyncState,
} from 'matrix-js-sdk';
import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
import { Membership, NotificationType, StateEvent } from '../../../types/matrix/room';
import {
  getNotificationType,
  getUnreadInfo,
  getUnreadInfos,
  isNotificationEvent,
} from '../../utils/room';
import { roomToUnreadAtom } from './roomToUnread';
import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useSyncState } from '../../hooks/useSyncState';
import { useRoomsNotificationPreferencesContext } from '../../hooks/useRoomsNotificationPreferences';

// Wires the SDK signals (Timeline, Receipt, UnreadNotifications,
// MyMembership, sync state, space-child) into `roomToUnreadAtom`.
// Split from the atom file so the reducer + atom shape stay free of
// MatrixClient / React imports — see `roomToUnread.ts` for the data
// layer.
export const useBindRoomToUnreadAtom = (
  mx: MatrixClient,
  unreadAtom: typeof roomToUnreadAtom,
  unreadThreadingEnabled: boolean
) => {
  const setUnreadAtom = useSetAtom(unreadAtom);
  const roomsNotificationPreferences = useRoomsNotificationPreferencesContext();

  useEffect(() => {
    setUnreadAtom({
      type: 'RESET',
      unreadInfos: getUnreadInfos(mx),
    });
  }, [mx, setUnreadAtom]);

  useSyncState(
    mx,
    useCallback(
      (state, prevState) => {
        if (
          (state === SyncState.Prepared && prevState === null) ||
          (state === SyncState.Syncing && prevState !== SyncState.Syncing)
        ) {
          setUnreadAtom({
            type: 'RESET',
            unreadInfos: getUnreadInfos(mx),
          });
        }
      },
      [mx, setUnreadAtom]
    )
  );

  useEffect(() => {
    const handleTimelineEvent = (
      mEvent: MatrixEvent,
      room: Room | undefined,
      toStartOfTimeline: boolean | undefined,
      removed: boolean,
      data: IRoomTimelineData
    ) => {
      if (!room || !data.liveEvent || room.isSpaceRoom() || !isNotificationEvent(mEvent)) return;
      if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
        setUnreadAtom({
          type: 'DELETE',
          roomId: room.roomId,
        });
        return;
      }

      if (mEvent.getSender() === mx.getUserId()) return;
      setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) });
    };
    mx.on(RoomEvent.Timeline, handleTimelineEvent);
    return () => {
      mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
    };
  }, [mx, setUnreadAtom]);

  useEffect(() => {
    const handleReceipt = (mEvent: MatrixEvent, room: Room) => {
      const myUserId = mx.getUserId();
      if (!myUserId) return;
      if (room.isSpaceRoom()) return;
      const content = mEvent.getContent<ReceiptContent>();

      const isMyReceipt = Object.keys(content).find((eventId) =>
        (Object.keys(content[eventId]) as ReceiptType[]).find(
          (receiptType) => content[eventId][receiptType][myUserId]
        )
      );
      if (!isMyReceipt) return;

      // Pre-M4 fallback: blind DELETE wipes the whole room's unread on
      // any own receipt. Acceptable when threading is disabled because
      // there are no per-thread receipts to honour separately.
      if (!unreadThreadingEnabled) {
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
        return;
      }

      // Mute gate — pre-M4 the blind-DELETE path implicitly respected
      // mute (DELETE is a no-op for muted rooms which were never in the
      // atom). PUT-with-fresh would re-introduce a muted room into the
      // unread index whenever its `getUnreadInfo` returned non-zero
      // (e.g. server-side counters for a muted room still tick on
      // pings). Mirrors the mute branch in the Timeline + UnreadNotifications
      // handlers above/below.
      if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
        return;
      }

      // With per-thread unread on, an own receipt no longer implies the
      // user read every thread in the room — SDK already partitioned
      // counters by thread_id before firing this event, so trust it.
      // `getUnreadInfo` reads `room.getUnreadNotificationCount(Total)`
      // which sums main + all threads. If the receipt was for the main
      // timeline, threaded counts stay; if for a thread, the rest stays.
      const fresh = getUnreadInfo(room);
      if (fresh.total === 0 && fresh.highlight === 0) {
        // Explicit DELETE: putUnreadInfo would write a `{ total: 0,
        // highlight: 0, from: null }` entry into the Map, which keeps
        // the room in the index and breaks parent rollup deltas on the
        // next PUT.
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
      } else {
        setUnreadAtom({ type: 'PUT', unreadInfo: fresh });
      }
    };
    mx.on(RoomEvent.Receipt, handleReceipt);
    return () => {
      mx.removeListener(RoomEvent.Receipt, handleReceipt);
    };
  }, [mx, setUnreadAtom, unreadThreadingEnabled]);

  // RoomEvent.UnreadNotifications is the canonical signal for unread
  // count change — fired by the SDK whenever main-timeline or
  // per-thread totals shift (room.js:1354 / 1398 / 1426). Catches three
  // scenarios the Receipt + Timeline handlers miss:
  //
  // (a) Local-echo own receipt: `addLocalEchoReceipt` emits
  //     `RoomEvent.Receipt` synchronously but only zeros notification
  //     counters when reading own last event AND non-synthetic — for
  //     drawer-fired thread receipts, counters stay stale until the
  //     server echoes back via /sync. The /sync echo updates counters
  //     before firing UnreadNotifications, so handler reads fresh.
  // (b) Cross-device own receipt arriving via /sync ephemeral.
  // (c) Encrypted-room decryption fixing notification counts after the
  //     fact (`fixNotificationCountOnDecryption` in client.js).
  //
  // Per-Room event — not re-emitted to the client. Attach to existing
  // rooms on mount + listen to ClientEvent.Room for new joins. Mirrors
  // pattern in roomToParents.ts. Skipped when the kill switch is off
  // — pre-M4 only listened to Receipt (blind DELETE), so the legacy
  // path stays bit-equivalent without this listener.
  useEffect(() => {
    if (!unreadThreadingEnabled) return undefined;

    const recompute = (room: Room) => {
      if (room.isSpaceRoom()) return;
      // Skip rooms the user no longer belongs to — late UnreadNotifications
      // emits (e.g. queued before MyMembership DELETE flushed) would
      // otherwise re-PUT a left room into the unread index.
      if (room.getMyMembership() !== Membership.Join) return;
      if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
        return;
      }
      const fresh = getUnreadInfo(room);
      if (fresh.total === 0 && fresh.highlight === 0) {
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
      } else {
        setUnreadAtom({ type: 'PUT', unreadInfo: fresh });
      }
    };

    const handlers = new Map<Room, () => void>();
    const attach = (room: Room) => {
      if (handlers.has(room)) return;
      const handler = () => recompute(room);
      room.on(RoomEvent.UnreadNotifications, handler);
      handlers.set(room, handler);
    };
    const detach = (room: Room) => {
      const handler = handlers.get(room);
      if (!handler) return;
      room.removeListener(RoomEvent.UnreadNotifications, handler);
      handlers.delete(room);
    };
    const handleNewRoom = (room: Room) => attach(room);
    // ClientEvent.DeleteRoom fires when the SDK drops a room (rejected
    // invite, forget, etc) — release the per-room handler ref so the
    // Map doesn't grow unbounded across long-running sessions.
    const handleDeleteRoom = (deletedRoomId: string) => {
      handlers.forEach((_handler, room) => {
        if (room.roomId === deletedRoomId) detach(room);
      });
    };

    mx.getRooms().forEach(attach);
    mx.on(ClientEvent.Room, handleNewRoom);
    mx.on(ClientEvent.DeleteRoom, handleDeleteRoom);

    return () => {
      mx.removeListener(ClientEvent.Room, handleNewRoom);
      mx.removeListener(ClientEvent.DeleteRoom, handleDeleteRoom);
      handlers.forEach((handler, room) => {
        room.removeListener(RoomEvent.UnreadNotifications, handler);
      });
      handlers.clear();
    };
  }, [mx, setUnreadAtom, unreadThreadingEnabled]);

  useEffect(() => {
    setUnreadAtom({
      type: 'RESET',
      unreadInfos: getUnreadInfos(mx),
    });
  }, [mx, setUnreadAtom, roomsNotificationPreferences]);

  useEffect(() => {
    const handleMembershipChange = (room: Room, membership: string) => {
      if (membership !== Membership.Join) {
        setUnreadAtom({
          type: 'DELETE',
          roomId: room.roomId,
        });
      }
    };
    mx.on(RoomEvent.MyMembership, handleMembershipChange);
    return () => {
      mx.removeListener(RoomEvent.MyMembership, handleMembershipChange);
    };
  }, [mx, setUnreadAtom]);

  useStateEventCallback(
    mx,
    useCallback(
      (mEvent) => {
        if (mEvent.getType() === StateEvent.SpaceChild) {
          setUnreadAtom({
            type: 'RESET',
            unreadInfos: getUnreadInfos(mx),
          });
        }
      },
      [mx, setUnreadAtom]
    )
  );
};
