import { produce } from 'immer';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import {
  IRoomTimelineData,
  MatrixClient,
  MatrixEvent,
  Room,
  RoomEvent,
  SyncState,
} from 'matrix-js-sdk';
import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
import { useCallback, useEffect, useRef } from 'react';
import {
  Membership,
  NotificationType,
  RoomToUnread,
  UnreadInfo,
  Unread,
  StateEvent,
} from '../../../types/matrix/room';
import {
  getAllParents,
  getNotificationType,
  getUnreadInfo,
  getUnreadInfos,
  isNotificationEvent,
} from '../../utils/room';
import { roomToParentsAtom } from './roomToParents';
import { useStateEventCallback } from '../../hooks/useStateEventCallback';
import { useSyncState } from '../../hooks/useSyncState';
import { useRoomsNotificationPreferencesContext } from '../../hooks/useRoomsNotificationPreferences';
import { useRoomsListener } from '../../hooks/useRoomsListener';
import { threadNotificationsAtom } from '../threadNotifications';
import { getMutedThreads } from '../../utils/threadNotifications';

export type RoomToUnreadAction =
  | {
      type: 'RESET';
      unreadInfos: UnreadInfo[];
    }
  | {
      type: 'PUT';
      unreadInfo: UnreadInfo;
    }
  | {
      type: 'DELETE';
      roomId: string;
    };

export const unreadInfoToUnread = (unreadInfo: UnreadInfo): Unread => ({
  highlight: unreadInfo.highlight,
  total: unreadInfo.total,
  from: null,
});

const putUnreadInfo = (
  roomToUnread: RoomToUnread,
  allParents: Set<string>,
  unreadInfo: UnreadInfo,
) => {
  const oldUnread = roomToUnread.get(unreadInfo.roomId) ?? { highlight: 0, total: 0, from: null };
  roomToUnread.set(unreadInfo.roomId, unreadInfoToUnread(unreadInfo));

  const newH = unreadInfo.highlight - oldUnread.highlight;
  const newT = unreadInfo.total - oldUnread.total;

  allParents.forEach((parentId) => {
    const oldParentUnread = roomToUnread.get(parentId) ?? { highlight: 0, total: 0, from: null };
    roomToUnread.set(parentId, {
      highlight: (oldParentUnread.highlight += newH),
      total: (oldParentUnread.total += newT),
      from: new Set([...(oldParentUnread.from ?? []), unreadInfo.roomId]),
    });
  });
};

const deleteUnreadInfo = (roomToUnread: RoomToUnread, allParents: Set<string>, roomId: string) => {
  const oldUnread = roomToUnread.get(roomId);
  if (!oldUnread) return;
  roomToUnread.delete(roomId);

  allParents.forEach((parentId) => {
    const oldParentUnread = roomToUnread.get(parentId);
    if (!oldParentUnread) return;
    // `from` is always a Set for parent aggregates; the fallback must be an
    // iterable of ids, NOT the roomId string (which would spread into chars).
    const newFrom = new Set([...(oldParentUnread.from ?? [])]);
    newFrom.delete(roomId);
    if (newFrom.size === 0) {
      roomToUnread.delete(parentId);
      return;
    }
    roomToUnread.set(parentId, {
      highlight: oldParentUnread.highlight - oldUnread.highlight,
      total: oldParentUnread.total - oldUnread.total,
      from: newFrom,
    });
  });
};

export const unreadEqual = (u1: Unread, u2: Unread): boolean => {
  const countEqual = u1.highlight === u2.highlight && u1.total === u2.total;

  if (!countEqual) return false;

  const f1 = u1.from;
  const f2 = u2.from;
  if (f1 === null && f2 === null) return true;
  if (f1 === null || f2 === null) return false;

  if (f1.size !== f2.size) return false;

  let fromEqual = true;
  f1?.forEach((item) => {
    if (!f2?.has(item)) {
      fromEqual = false;
    }
  });

  return fromEqual;
};

const baseRoomToUnread = atom<RoomToUnread>(new Map());
export const roomToUnreadAtom = atom<RoomToUnread, [RoomToUnreadAction], undefined>(
  (get) => get(baseRoomToUnread),
  (get, set, action) => {
    if (action.type === 'RESET') {
      const draftRoomToUnread: RoomToUnread = new Map();
      action.unreadInfos.forEach((unreadInfo) => {
        putUnreadInfo(
          draftRoomToUnread,
          getAllParents(get(roomToParentsAtom), unreadInfo.roomId),
          unreadInfo,
        );
      });
      set(baseRoomToUnread, draftRoomToUnread);
      return;
    }
    if (action.type === 'PUT') {
      const { unreadInfo } = action;
      // A { total: 0, highlight: 0 } entry is still a *present* map key, and the
      // nav dot lights on any present entry — so a phantom zero-count PUT (e.g.
      // the UnreadNotifications listener firing once the server zeroes counts)
      // would leave a stuck dot. Collapse it to a DELETE so a fully-read room
      // actually clears. Done before the unreadEqual short-circuit so an
      // already-stuck { 0, 0 } gets removed too.
      if (unreadInfo.total === 0 && unreadInfo.highlight === 0) {
        if (get(baseRoomToUnread).has(unreadInfo.roomId)) {
          set(
            baseRoomToUnread,
            produce(get(baseRoomToUnread), (draftRoomToUnread) =>
              deleteUnreadInfo(
                draftRoomToUnread,
                getAllParents(get(roomToParentsAtom), unreadInfo.roomId),
                unreadInfo.roomId,
              ),
            ),
          );
        }
        return;
      }
      const currentUnread = get(baseRoomToUnread).get(unreadInfo.roomId);
      if (currentUnread && unreadEqual(currentUnread, unreadInfoToUnread(unreadInfo))) {
        // Do not update if unread data has not changes
        // like total & highlight
        return;
      }
      set(
        baseRoomToUnread,
        produce(get(baseRoomToUnread), (draftRoomToUnread) =>
          putUnreadInfo(
            draftRoomToUnread,
            getAllParents(get(roomToParentsAtom), unreadInfo.roomId),
            unreadInfo,
          ),
        ),
      );
      return;
    }
    if (action.type === 'DELETE' && get(baseRoomToUnread).has(action.roomId)) {
      set(
        baseRoomToUnread,
        produce(get(baseRoomToUnread), (draftRoomToUnread) =>
          deleteUnreadInfo(
            draftRoomToUnread,
            getAllParents(get(roomToParentsAtom), action.roomId),
            action.roomId,
          ),
        ),
      );
    }
  },
);

export const useBindRoomToUnreadAtom = (mx: MatrixClient, unreadAtom: typeof roomToUnreadAtom) => {
  const setUnreadAtom = useSetAtom(unreadAtom);
  const roomsNotificationPreferences = useRoomsNotificationPreferencesContext();
  const threadNotifications = useAtomValue(threadNotificationsAtom);
  // Latest thread-notification prefs for the SDK-event handlers below, read via a
  // ref so changing prefs never re-attaches the (many) per-room listeners. The
  // dedicated reset effect keyed on `threadNotifications` handles mute/unmute.
  const threadNotificationsRef = useRef(threadNotifications);
  threadNotificationsRef.current = threadNotifications;

  useEffect(() => {
    setUnreadAtom({
      type: 'RESET',
      unreadInfos: getUnreadInfos(mx, threadNotificationsRef.current),
    });
  }, [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, threadNotificationsRef.current),
          });
        }
      },
      [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;
      // Single-owner rule: thread replies drive the room badge via
      // RoomEvent.UnreadNotifications below — ignore them here so the count is
      // never double-driven / mis-attributed to the main timeline.
      if (mEvent.threadRootId && mEvent.getId() !== mEvent.threadRootId) 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,
          getMutedThreads(threadNotificationsRef.current, room.roomId),
          mx,
        ),
      });
    };
    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) {
        // Optimistically clear on our own receipt (upstream cinny behavior).
        // Do NOT recompute from getUnreadInfo here: getUnreadNotificationCount is
        // server-computed and STALE on the synchronous synthetic receipt echo
        // (the SDK only zeroes it immediately when the last live event is our own
        // message), so recomputing PUTs the stale non-zero count back → the dot
        // sticks / resurrects. The RoomEvent.UnreadNotifications listener below
        // re-asserts the accurate badge (incl. restoring the main badge after a
        // thread read) once the server acks, and a { 0, 0 } PUT collapses to a
        // DELETE in the reducer.
        setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
      }
    };
    mx.on(RoomEvent.Receipt, handleReceipt);
    return () => {
      mx.removeListener(RoomEvent.Receipt, handleReceipt);
    };
  }, [mx, setUnreadAtom]);

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

  // Mute/unmute of a thread rewrites `threadNotificationsAtom`; recompute badges
  // immediately so muted-thread subtraction takes effect without waiting for the
  // next timeline / unread event (mirrors the notification-preferences reset).
  useEffect(() => {
    setUnreadAtom({
      type: 'RESET',
      unreadInfos: getUnreadInfos(mx, threadNotifications),
    });
  }, [mx, setUnreadAtom, threadNotifications]);

  // RoomEvent.UnreadNotifications is emitted room-level only (never re-emitted
  // client-side), so the main Timeline pathway misses thread-count changes and
  // room badges lag. useRoomsListener PREPENDS the emitting Room (the SDK emits
  // this event with variable arity — 0/1/2 args — so only a leading slot is
  // positionally stable), making this a surgical per-room PUT with muted-thread
  // subtraction re-applied. Room-mute keeps its DELETE semantics.
  useRoomsListener(
    mx,
    RoomEvent.UnreadNotifications,
    useCallback(
      (room: Room) => {
        if (room.isSpaceRoom()) return;
        if (getNotificationType(mx, room.roomId) === NotificationType.Mute) {
          setUnreadAtom({ type: 'DELETE', roomId: room.roomId });
          return;
        }
        setUnreadAtom({
          type: 'PUT',
          unreadInfo: getUnreadInfo(
            room,
            getMutedThreads(threadNotificationsRef.current, room.roomId),
            mx,
          ),
        });
      },
      [mx, setUnreadAtom],
    ),
  );

  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, threadNotificationsRef.current),
          });
        }
      },
      [mx, setUnreadAtom],
    ),
  );
};
