import { useEffect, useMemo, useState } from 'react';
import { MatrixClient, 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(),
});

type PresenceListener = () => void;

/**
 * Shared presence store. Previously every avatar's `useUserPresence` registered
 * its own 3 client listeners (Presence / CurrentlyActive / LastPresenceTs) — on
 * a big room that meant hundreds of global listeners, each presence event
 * fanning out across all of them, with churn on every fast scroll (PERF-1).
 *
 * This registers exactly 3 listeners total (once, on the first subscriber) and
 * fans out to per-user subscribers itself. Subscribing on the client rather
 * than per-`User` object also avoids the SDK's 10-listener-per-User cap.
 */
class PresenceStore {
  private mx: MatrixClient | undefined;

  private started = false;

  // userId -> set of component callbacks to notify on change.
  private subscribers = new Map<string, Set<PresenceListener>>();

  // userId -> last computed presence (stable reference until it changes), only
  // retained while at least one subscriber for that user is mounted.
  private cache = new Map<string, UserPresence | undefined>();

  private handleEvent: UserEventHandlerMap[UserEvent.Presence] = (_event, user) => {
    const subs = this.subscribers.get(user.userId);
    if (!subs || subs.size === 0) return; // ignore users nothing is watching
    this.cache.set(user.userId, getUserPresence(user));
    subs.forEach((cb) => cb());
  };

  private start(mx: MatrixClient): void {
    if (this.started && this.mx === mx) return;
    // A different client (re-login without reload) — drop the old listeners.
    if (this.started && this.mx) {
      this.mx.removeListener(UserEvent.Presence, this.handleEvent);
      this.mx.removeListener(UserEvent.CurrentlyActive, this.handleEvent);
      this.mx.removeListener(UserEvent.LastPresenceTs, this.handleEvent);
      this.cache.clear();
    }
    this.mx = mx;
    mx.on(UserEvent.Presence, this.handleEvent);
    mx.on(UserEvent.CurrentlyActive, this.handleEvent);
    mx.on(UserEvent.LastPresenceTs, this.handleEvent);
    this.started = true;
  }

  subscribe(mx: MatrixClient, userId: string, cb: PresenceListener): () => void {
    this.start(mx);
    let subs = this.subscribers.get(userId);
    if (!subs) {
      subs = new Set();
      this.subscribers.set(userId, subs);
    }
    subs.add(cb);
    return () => {
      subs.delete(cb);
      // Only tear down the registry entry if this is still the live set for the
      // user — keeps unsubscribe idempotent so a double-invoke (or a later
      // re-subscribe that replaced the set) can't evict a newer subscriber.
      if (subs.size === 0 && this.subscribers.get(userId) === subs) {
        this.subscribers.delete(userId);
        this.cache.delete(userId);
      }
    };
  }

  get(mx: MatrixClient, userId: string): UserPresence | undefined {
    if (this.cache.has(userId)) return this.cache.get(userId);
    const user = mx.getUser(userId);
    const presence = user ? getUserPresence(user) : undefined;
    // Only retain in the cache while something is actually subscribed, so a
    // render that never commits (and thus never subscribes) can't leave a
    // dangling entry. handleEvent keeps it fresh thereafter.
    if (this.subscribers.get(userId)?.size) this.cache.set(userId, presence);
    return presence;
  }
}

const presenceStore = new PresenceStore();

export const useUserPresence = (userId: string): UserPresence | undefined => {
  const mx = useMatrixClient();
  const [presence, setPresence] = useState(() => presenceStore.get(mx, userId));

  useEffect(() => {
    // Re-seed synchronously (userId/mx may have changed, or a late-arriving User
    // object now exists), then track changes via the shared store.
    setPresence(presenceStore.get(mx, userId));
    return presenceStore.subscribe(mx, userId, () => {
      setPresence(presenceStore.get(mx, userId));
    });
  }, [mx, userId]);

  return presence;
};

export const usePresenceLabel = (): Record<Presence, string> =>
  useMemo(
    // Keep this vocabulary aligned with the status setter (PresencePicker in
    // SettingsTab.tsx): online -> "Online", unavailable -> "Idle". Previously
    // these read "Active"/"Busy"/"Away", so a user who set "Idle" showed as
    // "Busy" to others.
    () => ({
      [Presence.Online]: 'Online',
      [Presence.Unavailable]: 'Idle',
      [Presence.Offline]: 'Offline',
    }),
    [],
  );
