// DM call entry point that unifies start, join and switch flows.
//
// Contract:
//   - no prev embed       → start a new DM call
//   - prev.roomId === arg, healthy (joined + peer present) → no-op
//   - prev.roomId === arg, zombie (never joined, peer gone, or stuck) →
//     dispose prev directly and start fresh; waitLeave is skipped because
//     the widget is likely unresponsive
//   - prev.roomId !== arg → switch: hangup prev, wait for clean leave,
//     dispose prev, then start new
//
// Switch barrier waits for one of two success signals:
//   1. MembershipsChanged removing our (user, device) from prev room session
//      — server-authoritative, this is what the peer actually observes
//   2. action:im.vector.hangup from the widget — widget-side confirmation,
//      including pre-join / not-yet-member states where there is nothing to
//      observe via MembershipsChanged
//   3. 3s timeout — fail-closed: abort switch, do not start the new call
//
// Listeners are armed BEFORE hangup() to avoid missing a fast action ack.
//
// The function is serialized via inFlightRef: concurrent double-taps await the
// same promise and then re-enter. After re-entry the same-room branch usually
// makes the second tap a noop, matching user intent.

import { useCallback, useRef } from 'react';
import { useAtomValue, useStore } from 'jotai';
import { CallMembership } from 'matrix-js-sdk/lib/matrixrtc/CallMembership';
import { MatrixRTCSessionEvent } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
import { useMatrixClient } from './useMatrixClient';
import { useTheme } from './useTheme';
import { createCallEmbed, useCallEmbedRef } from './useCallEmbed';
import { useCallPreferencesAtom } from '../state/hooks/callPreferences';
import { callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';

const SWITCH_LEAVE_TIMEOUT_MS = 3000;

const createSwitchTimeoutError = (roomId: string): Error =>
  new Error(`[dm-call] switch timed out waiting for clean leave in ${roomId}`);

export const useSwitchOrStartDmCall = (): ((roomId: string) => Promise<void>) => {
  const mx = useMatrixClient();
  const theme = useTheme();
  const store = useStore();
  const callEmbedRef = useCallEmbedRef();
  const callPref = useAtomValue(useCallPreferencesAtom());

  const inFlightRef = useRef<Promise<void> | undefined>(undefined);

  const waitLeave = useCallback(
    (prev: CallEmbed): Promise<void> => {
      const session = mx.matrixRTC.getRoomSession(prev.room);
      const selfUserId = mx.getSafeUserId();
      const selfDeviceId = mx.getDeviceId();
      const selfPresent = (memberships: CallMembership[]): boolean =>
        memberships.some(
          (m) => m.userId === selfUserId && (!selfDeviceId || m.deviceId === selfDeviceId)
        );

      return new Promise<void>((resolve, reject) => {
        let done = false;
        // Assigned once listeners are wired — set to a real cleanup below
        // so the pre-wire window is never reachable (finish can only fire
        // after the listeners/timer exist).
        let cleanup: () => void = () => undefined;
        const finish = () => {
          if (done) return;
          done = true;
          cleanup();
          resolve();
        };
        const fail = () => {
          if (done) return;
          done = true;
          cleanup();
          reject(createSwitchTimeoutError(prev.roomId));
        };

        const onMemberships = (_old: CallMembership[], next: CallMembership[]): void => {
          if (!selfPresent(next)) finish();
        };
        const onHangupAction = (): void => finish();

        session.on(MatrixRTCSessionEvent.MembershipsChanged, onMemberships);
        prev.call.on('action:im.vector.hangup', onHangupAction);
        const timer = setTimeout(fail, SWITCH_LEAVE_TIMEOUT_MS);

        cleanup = () => {
          session.off(MatrixRTCSessionEvent.MembershipsChanged, onMemberships);
          prev.call.off('action:im.vector.hangup', onHangupAction);
          clearTimeout(timer);
        };

        prev.hangup().catch((err: unknown) => {
          // eslint-disable-next-line no-console
          console.warn('[dm-call] hangup transport fail (barrier still armed)', err);
        });
      });
    },
    [mx]
  );

  const doSwitchOrStart = useCallback(
    async (roomId: string): Promise<void> => {
      const room = mx.getRoom(roomId);
      if (!room) {
        // eslint-disable-next-line no-console
        console.warn('[dm-call] room not found', roomId);
        return;
      }

      const prev = store.get(callEmbedAtom);
      if (prev?.roomId === roomId) {
        const selfUserId = mx.getSafeUserId();
        const selfDeviceId = mx.getDeviceId();
        const peerPresent = mx.matrixRTC
          .getRoomSession(prev.room)
          .memberships.some(
            (m: CallMembership) =>
              m.userId !== selfUserId || (!!selfDeviceId && m.deviceId !== selfDeviceId)
          );
        if (prev.joined && peerPresent) return;
      }

      const container = callEmbedRef.current;
      if (!container) {
        throw new Error('Failed to start call, No embed container element found!');
      }

      if (prev) {
        if (prev.roomId === roomId) {
          // Zombie same-room embed (widget never joined, peer gone, or stuck
          // after LiveKit reconnect fail). waitLeave would hang on the
          // unresponsive widget. Best-effort widget hangup before direct
          // dispose so the widget's MembershipManager gets a chance to send
          // the server-side leave event; if it doesn't, the SDK's delayed
          // leave fires a few seconds after dispose.
          if (prev.joined) {
            prev.hangup().catch(() => undefined);
          }
          prev.dispose();
        } else {
          await waitLeave(prev);
          prev.dispose();
        }
      }

      const embed = createCallEmbed(mx, room, true, theme.kind, container, callPref, true);
      store.set(callEmbedAtom, embed);
    },
    [mx, store, callEmbedRef, callPref, theme, waitLeave]
  );

  return useCallback(
    (roomId: string): Promise<void> => {
      const enter = (): Promise<void> => {
        if (inFlightRef.current) {
          return inFlightRef.current.then(enter, enter);
        }
        const task = doSwitchOrStart(roomId);
        const tracked: Promise<void> = task.finally(() => {
          if (inFlightRef.current === tracked) inFlightRef.current = undefined;
        });
        inFlightRef.current = tracked;
        return tracked;
      };
      return enter();
    },
    [doSwitchOrStart]
  );
};
