import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
import { User, UserEvent, UserEventHandlerMap } from 'matrix-js-sdk';
import { useMatrixClient } from './useMatrixClient';

export enum Presence {
  Online = 'online',
  Unavailable = 'unavailable',
  Offline = 'offline',
}

export type UserPresence = {
  presence: Presence;
  status?: string;
  active: boolean;
  lastActiveTs?: number;
};

const getUserPresence = (user: User): UserPresence => ({
  presence: user.presence as Presence,
  status: user.presenceStatusMsg,
  active: user.currentlyActive,
  lastActiveTs: user.getLastActiveTs(),
});

const presenceEqual = (a: UserPresence | undefined, b: UserPresence | undefined): boolean => {
  if (a === b) return true;
  if (!a || !b) return false;
  return (
    a.presence === b.presence &&
    a.status === b.status &&
    a.active === b.active &&
    a.lastActiveTs === b.lastActiveTs
  );
};

type PresenceEntry = {
  /** Stable reference — only replaced when presence data changes. */
  presence: UserPresence;
  listeners: Set<() => void>;
  user: User;
  onChange: UserEventHandlerMap[UserEvent.Presence];
};

/** One EventEmitter subscription per userId, shared across all hook consumers. */
const presenceEntries = new Map<string, PresenceEntry>();

const subscribeNoop = () => () => undefined;

const bindUser = (userId: string, user: User): PresenceEntry => {
  const existing = presenceEntries.get(userId);
  if (existing) {
    if (existing.user === user) return existing;
    existing.user.removeListener(UserEvent.Presence, existing.onChange);
    existing.user.removeListener(UserEvent.CurrentlyActive, existing.onChange);
    existing.user.removeListener(UserEvent.LastPresenceTs, existing.onChange);
    presenceEntries.delete(userId);
  }

  const entry: PresenceEntry = {
    presence: getUserPresence(user),
    listeners: new Set(),
    user,
    onChange: () => undefined,
  };

  entry.onChange = (_event, u) => {
    const current = presenceEntries.get(userId);
    if (!current || u.userId !== current.user.userId) return;
    const next = getUserPresence(current.user);
    if (presenceEqual(current.presence, next)) return;
    current.presence = next;
    current.listeners.forEach((listener) => listener());
  };

  user.on(UserEvent.Presence, entry.onChange);
  user.on(UserEvent.CurrentlyActive, entry.onChange);
  user.on(UserEvent.LastPresenceTs, entry.onChange);
  presenceEntries.set(userId, entry);
  return entry;
};

const releaseIfUnused = (userId: string) => {
  const entry = presenceEntries.get(userId);
  if (!entry || entry.listeners.size > 0) return;
  entry.user.removeListener(UserEvent.Presence, entry.onChange);
  entry.user.removeListener(UserEvent.CurrentlyActive, entry.onChange);
  entry.user.removeListener(UserEvent.LastPresenceTs, entry.onChange);
  presenceEntries.delete(userId);
};

/**
 * Live presence for a Matrix user. Safe to call from every message row —
 * listeners are demuxed to one EventEmitter subscription per userId.
 */
export const useUserPresence = (userId: string): UserPresence | undefined => {
  const mx = useMatrixClient();
  // Cache getSnapshot result so useSyncExternalStore doesn't loop.
  const snapshotRef = useRef<UserPresence | undefined>(undefined);

  const subscribe = useCallback(
    (onStoreChange: () => void) => {
      if (!userId) return subscribeNoop();

      const user = mx.getUser(userId);
      if (!user) return subscribeNoop();

      const entry = bindUser(userId, user);
      snapshotRef.current = entry.presence;
      entry.listeners.add(onStoreChange);
      return () => {
        entry.listeners.delete(onStoreChange);
        releaseIfUnused(userId);
      };
    },
    [mx, userId]
  );

  const getSnapshot = useCallback(() => {
    if (!userId) {
      snapshotRef.current = undefined;
      return undefined;
    }
    const cached = presenceEntries.get(userId);
    if (cached) {
      snapshotRef.current = cached.presence;
      return cached.presence;
    }
    const user = mx.getUser(userId);
    if (!user) {
      snapshotRef.current = undefined;
      return undefined;
    }
    // Not subscribed yet — return a stable fallback without allocating each read
    // once we have a previous snapshot that still matches.
    const next = getUserPresence(user);
    if (presenceEqual(snapshotRef.current, next)) {
      return snapshotRef.current;
    }
    snapshotRef.current = next;
    return next;
  }, [mx, userId]);

  return useSyncExternalStore(subscribe, getSnapshot, () => undefined);
};

export const usePresenceLabel = (): Record<Presence, string> =>
  useMemo(
    () => ({
      [Presence.Online]: 'Active',
      [Presence.Unavailable]: 'Busy',
      [Presence.Offline]: 'Away',
    }),
    []
  );
