import { createContext, RefObject, useCallback, useContext, useEffect, useState } from 'react';
import { MatrixClient, Room } from 'matrix-js-sdk';
import { useSetAtom } from 'jotai';
import {
  CallEmbed,
  CallLoadErrorReason,
  ElementCallThemeKind,
  ElementWidgetActions,
  useClientWidgetApiEvent,
} from '../plugins/call';
import { useMatrixClient } from './useMatrixClient';
import { ThemeKind, useTheme } from './useTheme';
import { callEmbedAtom } from '../state/callEmbed';
import { useResizeObserver } from './useResizeObserver';
import { CallControlState } from '../plugins/call/CallControlState';
import { useCallMembersChange, useCallSession } from './useCall';
import { CallPreferences } from '../state/callPreferences';
import { useSetting } from '../state/hooks/settings';
import { NoiseSuppressionMode, settingsAtom } from '../state/settings';
import { unlockCallSounds } from '../utils/callSounds';

const CallEmbedContext = createContext<CallEmbed | undefined>(undefined);

export const CallEmbedContextProvider = CallEmbedContext.Provider;

export const useCallEmbed = (): CallEmbed | undefined => {
  const callEmbed = useContext(CallEmbedContext);

  return callEmbed;
};

const CallEmbedRefContext = createContext<RefObject<HTMLDivElement> | undefined>(undefined);
export const CallEmbedRefContextProvider = CallEmbedRefContext.Provider;
export const useCallEmbedRef = (): RefObject<HTMLDivElement> => {
  const ref = useContext(CallEmbedRefContext);
  if (!ref) {
    throw new Error('CallEmbedRef is not provided!');
  }
  return ref;
};

export const createCallEmbed = (
  mx: MatrixClient,
  room: Room,
  dm: boolean,
  themeKind: ElementCallThemeKind,
  container: HTMLElement,
  pref?: CallPreferences,
  denoiseMode: NoiseSuppressionMode = 'browser',
  denoiseModel: string = 'rnnoise',
  denoiseNativeNS: boolean = true,
  denoiseGate: boolean = false,
  denoiseGateThreshold: number = -45,
  forceAudioOff = false,
): CallEmbed => {
  const rtcSession = mx.matrixRTC.getRoomSession(room);
  const ongoing = rtcSession.memberships.length > 0;

  const intent = CallEmbed.getIntent(dm, ongoing, pref?.video);
  const initialAudio = forceAudioOff ? false : (pref?.microphone ?? true);
  const initialVideo = pref?.video ?? false;
  const widget = CallEmbed.getWidget(
    mx,
    room,
    intent,
    themeKind,
    denoiseMode,
    denoiseModel,
    denoiseNativeNS,
    denoiseGate,
    denoiseGateThreshold,
    initialAudio,
    initialVideo,
  );
  const controlState =
    pref && new CallControlState(forceAudioOff ? false : pref.microphone, pref.video, pref.sound);

  const embed = new CallEmbed(mx, room, widget, container, controlState, themeKind);

  return embed;
};

export const useCallStart = (dm = false) => {
  const mx = useMatrixClient();
  const theme = useTheme();
  const setCallEmbed = useSetAtom(callEmbedAtom);
  const callEmbedRef = useCallEmbedRef();
  const [callNoiseSuppression] = useSetting(settingsAtom, 'callNoiseSuppression');
  const [callDenoiseModel] = useSetting(settingsAtom, 'callDenoiseModel');
  const [callDenoiseNativeNS] = useSetting(settingsAtom, 'callDenoiseNativeNS');
  const [callDenoiseGate] = useSetting(settingsAtom, 'callDenoiseGate');
  const [callDenoiseGateThreshold] = useSetting(settingsAtom, 'callDenoiseGateThreshold');
  const [pttMode] = useSetting(settingsAtom, 'pttMode');

  const startCall = useCallback(
    (room: Room, pref?: CallPreferences) => {
      const container = callEmbedRef.current;
      if (!container) {
        throw new Error('Failed to start call, No embed container element found!');
      }
      // startCall is always invoked from a click/tap handler — unlock the Web
      // Audio context now (within the gesture) so join/leave sounds that fire
      // later, without any gesture, are audible to everyone in the call.
      unlockCallSounds();
      const callEmbed = createCallEmbed(
        mx,
        room,
        dm,
        theme.kind,
        container,
        pref,
        callNoiseSuppression ?? 'browser',
        callDenoiseModel ?? 'rnnoise',
        callDenoiseNativeNS ?? true,
        callDenoiseGate ?? false,
        callDenoiseGateThreshold ?? -45,
        !!pttMode,
      );

      setCallEmbed(callEmbed);
    },
    [
      mx,
      dm,
      theme,
      setCallEmbed,
      callEmbedRef,
      callNoiseSuppression,
      callDenoiseModel,
      callDenoiseNativeNS,
      callDenoiseGate,
      callDenoiseGateThreshold,
      pttMode,
    ],
  );

  return startCall;
};

export const useCallJoined = (embed?: CallEmbed): boolean => {
  const [joined, setJoined] = useState(embed?.joined ?? false);

  useClientWidgetApiEvent(
    embed?.call,
    ElementWidgetActions.JoinCall,
    useCallback(() => {
      setJoined(true);
    }, []),
  );

  useEffect(() => {
    // Re-seed from the (new) embed whenever it changes identity, not only when
    // it becomes undefined. Answering a 2nd call swaps embed A→B directly (embed
    // stays truthy), so a plain `if (!embed)` reset left `joined` stuck true and
    // B rendered as already-joined, skipping the loading/watchdog UI.
    setJoined(embed?.joined ?? false);
  }, [embed]);

  return joined;
};

/**
 * Surfaces a load failure (watchdog timeout or iframe error) from the embedded
 * Element Call iframe so the UI can show a recovery affordance instead of an
 * indefinite "Loading..." spinner.
 */
export const useCallLoadError = (embed?: CallEmbed): CallLoadErrorReason | undefined => {
  const [error, setError] = useState<CallLoadErrorReason | undefined>(() => embed?.loadFailed);

  useEffect(() => {
    if (!embed) {
      setError(undefined);
      return undefined;
    }
    setError(embed.loadFailed);
    return embed.onLoadError((reason) => setError(reason));
  }, [embed]);

  return error;
};

export const useCallHangupEvent = (embed: CallEmbed, callback: () => void) => {
  useClientWidgetApiEvent(embed.call, ElementWidgetActions.HangupCall, callback);
  useClientWidgetApiEvent(embed.call, ElementWidgetActions.Close, callback);
};

export const useCallMemberSoundSync = (embed: CallEmbed) => {
  const callSession = useCallSession(embed.room);
  useCallMembersChange(
    callSession,
    useCallback(() => embed.control.applySound(), [embed]),
  );
};

export const useCallThemeSync = (embed: CallEmbed) => {
  const theme = useTheme();

  useEffect(() => {
    const name: ElementCallThemeKind = theme.kind === ThemeKind.Dark ? 'dark' : 'light';

    embed.setTheme(name);
  }, [theme.kind, embed]);
};

export const useCallEmbedPlacementSync = (containerViewRef: RefObject<HTMLDivElement>): void => {
  const callEmbedRef = useCallEmbedRef();

  const syncCallEmbedPlacement = useCallback(() => {
    const embedEl = callEmbedRef.current;
    const container = containerViewRef.current;
    if (!embedEl || !container) return;

    const rect = container.getBoundingClientRect();
    embedEl.style.top = `${rect.top}px`;
    embedEl.style.left = `${rect.left}px`;
    embedEl.style.width = `${rect.width}px`;
    embedEl.style.height = `${rect.height}px`;
  }, [callEmbedRef, containerViewRef]);

  // Sync once on mount so the embed is positioned immediately (deps are stable refs)
  useEffect(() => {
    syncCallEmbedPlacement();
  }, [syncCallEmbedPlacement]);

  useResizeObserver(
    syncCallEmbedPlacement,
    useCallback(() => containerViewRef.current, [containerViewRef]),
  );
};
