import { useEffect, useMemo, useCallback, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { selectActiveSession, selectInSession } from '../store/features/activeSessionSlice';
import {
  selectChatMixer,
  selectBroadcastMixer,
  selectRecordingMixer,
  selectRecordingTrackMixers,
  selectBackingTrackMixers,
  selectJamTrackMixers,
  selectMetronomeTrackMixers,
  selectAdhocTrackMixers,
  selectMasterMixers,
  selectPersonalMixers,
  selectAllMixers,
  selectMixersByResourceId,
  selectMixersByTrackId,
  selectMetronome,
  selectMetronomeSettings,
  selectMediaSummary,
  selectNoAudioUsers,
  selectClientsWithAudioOverride,
  selectSimulatedMusicCategoryMixers,
  selectSimulatedChatCategoryMixers,
  selectMixersReady,
  setMasterMixers,
  setPersonalMixers,
  organizeMixers,
  updateMixer,
  setChatMixer,
  setBackingTracks as setBackingTracksAction,
  setJamTracks as setJamTracksAction,
  setRecordedTracks as setRecordedTracksAction,
  setMetronome as setMetronomeAction,
  setMediaSummary,
  setSimulatedMusicCategoryMixers as setSimulatedMusicAction,
  setSimulatedChatCategoryMixers as setSimulatedChatAction,
  setRecordingTrackMixers,
  setBackingTrackMixers,
  setJamTrackMixers,
  setMetronomeTrackMixers,
  setAdhocTrackMixers
} from '../store/features/mixersSlice';
import {
  selectBackingTracks,
  selectJamTracks,
  selectRecordedTracks
} from '../store/features/mediaSlice';
import {
  selectMixMode,
  selectCurrentMixerRange,
  setCurrentMixerRange
} from '../store/features/sessionUISlice';
import { ChannelGroupIds, CategoryGroupIds, MIX_MODES, MIDI_TRACK } from '../helpers/globals.js';
import useVuHelpers from './useVuHelpers.js';
import useFaderHelpers from './useFaderHelpers.js';
import useSessionHelper from './useSessionHelper.js';
import usePanHelpers from './usePanHelpers.js';
import { useJamServerContext } from '../context/JamServerContext.js';
import { useGlobalContext } from '../context/GlobalContext.js';
import { useVuContext } from '../context/VuContext.js';
import { getAvatarUrl, getInstrumentIcon45, getInstrumentIcon24 } from '../helpers/utils.js';


const useMixerHelper = () => {
  const dispatch = useDispatch();
  const allMixersRef = useRef({});
  const previousMyTracksRef = useRef([]);
  const previousMixerIdsRef = useRef(null);

  // Redux selectors - replace all useState calls
  const chatMixer = useSelector(selectChatMixer);
  const broadcastMixer = useSelector(selectBroadcastMixer);
  const recordingMixer = useSelector(selectRecordingMixer);
  const recordingTrackMixers = useSelector(selectRecordingTrackMixers);
  const backingTrackMixers = useSelector(selectBackingTrackMixers);
  const jamTrackMixers = useSelector(selectJamTrackMixers);
  const metronomeTrackMixers = useSelector(selectMetronomeTrackMixers);
  const adhocTrackMixers = useSelector(selectAdhocTrackMixers);
  const masterMixers = useSelector(selectMasterMixers);
  const personalMixers = useSelector(selectPersonalMixers);
  const allMixers = useSelector(selectAllMixers);
  const mixersByResourceId = useSelector(selectMixersByResourceId);
  const mixersByTrackId = useSelector(selectMixersByTrackId);
  const metronome = useSelector(selectMetronome);
  const metronomeSettings = useSelector(selectMetronomeSettings);
  const mediaSummary = useSelector(selectMediaSummary);
  const noAudioUsers = useSelector(selectNoAudioUsers);
  const clientsWithAudioOverride = useSelector(selectClientsWithAudioOverride);
  const simulatedMusicCategoryMixers = useSelector(selectSimulatedMusicCategoryMixers);
  const simulatedChatCategoryMixers = useSelector(selectSimulatedChatCategoryMixers);
  const isReadyRedux = useSelector(selectMixersReady);

  // Media data from mediaSlice
  const backingTracks = useSelector(selectBackingTracks);
  const jamTracks = useSelector(selectJamTracks);
  const recordedTracks = useSelector(selectRecordedTracks);

  // UI state from sessionUISlice
  const mixMode = useSelector(selectMixMode);
  const currentMixerRange = useSelector(selectCurrentMixerRange);

  const mediaTrackGroups = useMemo(() => [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup,
  ChannelGroupIds.MetronomeGroup], []);
  const muteBothMasterAndPersonalGroups = useMemo(() => [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MidiInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
  ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], []);

  // Phase 4: Replace CurrentSessionContext with Redux
  const currentSession = useSelector(selectActiveSession);
  const inSession = useSelector(selectInSession);
  const { jamClient, isConnected, server } = useJamServerContext();
  const { updateVU3, removeVuState } = useVuContext();
  const { getParticipant } = useSessionHelper();
  const { trackVolumeObject, setTrackVolumeObject } = useGlobalContext();
  const faderHelpers = useFaderHelpers();
  const panHelpers = usePanHelpers();

  const logger = console; // for now
  const context = window;

  const isReady = useRef(false);

  useEffect(() => {
    allMixersRef.current = allMixers;
    // console.log("_XDEBUG_ useMixerHelper: allMixersRef updated", allMixersRef.current);
  }, [allMixers]);

  const getMixer = useCallback((mixerId, mode) => {
    // Only default to mixMode if mode is undefined, not if it's explicitly false
    if (mode === undefined) {
      mode = mixMode;
    }
    return allMixersRef.current[(mode ? 'M' : 'P') + mixerId];
  }, [mixMode]);

  const fillTrackVolumeObject = useCallback((mixerId, mode, currentAllMixers, broadcast = true) => {
    const mixer = getMixer(mixerId, mode, currentAllMixers);
    if (mixer == null) {
      return null;
    }

    // Build volume object directly and return it (don't rely on async state update)
    const volumeObj = {
      id: mixer.id,
      _id: mixer._id,
      groupID: mixer.group_id,
      clientID: mixer.client_id,
      broadcast: broadcast,
      master: mixer.master,
      monitor: mixer.monitor,
      mute: mixer.mute,
      name: mixer.name,
      record: mixer.record,
      volL: mixer.volume_left,
      pan: mixer.pan,
      mediaType: mixer.media_type,
      isJamTrack: mixer.is_jam_track,
      isMetronome: mixer.is_metronome,
      volR: mixer.volume_left,
      loop: mixer.loop
    };

    // Also update React state for other consumers
    setTrackVolumeObject(volumeObj);

    // Redux: Update mixer range in sessionUISlice
    dispatch(setCurrentMixerRange({
      min: mixer.range_low,
      max: mixer.range_high
    }));

    // Return both mixer and the volume object
    return { mixer, volumeObj };
  }, [getMixer, setTrackVolumeObject, dispatch]);

  const setMixerVolume = useCallback(async (mixer, volumePercent, relative, originalVolume, controlGroup, baseVolumeObj) => {
    const newVolume = faderHelpers.convertPercentToAudioTaper(volumePercent);

    // Use the passed volumeObj instead of relying on async state
    const sourceVolumeObj = baseVolumeObj || trackVolumeObject;

    let updatedTrackVolumeObject;
    if (relative) {
      updatedTrackVolumeObject = {
        ...sourceVolumeObj,
        volL: sourceVolumeObj.volL + (newVolume - originalVolume),
        volR: sourceVolumeObj.volR + (newVolume - originalVolume),
      };

      // Apply clamping
      if (updatedTrackVolumeObject.volL < -80) updatedTrackVolumeObject.volL = -80;
      if (updatedTrackVolumeObject.volL > 20) updatedTrackVolumeObject.volL = 20;
      if (updatedTrackVolumeObject.volR < -80) updatedTrackVolumeObject.volR = -80;
      if (updatedTrackVolumeObject.volR > 20) updatedTrackVolumeObject.volR = 20;
    } else {
      updatedTrackVolumeObject = {
        ...sourceVolumeObj,
        volL: newVolume,
        volR: newVolume,
      };
    }

    // Update state
    setTrackVolumeObject(updatedTrackVolumeObject);

    // Use the computed object for jamClient call
    if (controlGroup != null) {
      const controlGroupsArg = mixer.mode === MIX_MODES.PERSONAL ? 0 : 1;
      console.log("setMixerVolume: setting session mixer category playout state for controlGroup", controlGroup, "controlGroupsArg", controlGroupsArg, "volume", updatedTrackVolumeObject.volL);
      await jamClient.setSessionMixerCategoryPlayoutState(controlGroup === 'music', controlGroupsArg, updatedTrackVolumeObject.volL);
    } else {
      await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedTrackVolumeObject);
    }
  }, [trackVolumeObject, faderHelpers, jamClient, setTrackVolumeObject]);

  const mediaMixers = useCallback((masterMixer, isOpener, currentAllMixers) => {
    const personalMixer = isOpener ? getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL, currentAllMixers) : masterMixer;
    const personalVuMixer = isOpener ? personalMixer : masterMixer;
    return {
      isOpener: isOpener,
      master: {
        mixer: masterMixer,
        muteMixer: masterMixer,
        vuMixer: masterMixer
      },
      personal: {
        mixer: personalMixer,
        muteMixer: personalMixer,
        vuMixer: personalVuMixer
      }
    };
  }, []);

  // Redux: updateMixerData now dispatches Redux actions
  const updateMixerData = useCallback((session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixMode) => {
    //console.debug("useMixerHelper: updateMixerData called", { session, masterMixers, personalMixers, mixMode });

    // Dispatch Redux actions instead of setState calls
    dispatch(setMasterMixers(masterMixers));
    dispatch(setPersonalMixers(personalMixers));
    // Note: session is tracked in activeSessionSlice, metro/noAudioUsers/clientsWithAudioOverride in mixersSlice
    // mixMode is tracked in sessionUISlice
  }, [dispatch]);

  // Redux: organizeMixers is now a reducer, trigger it via action
  useEffect(() => {
    if (!currentSession || masterMixers.length === 0 || personalMixers.length === 0) {
      return;
    }

    dispatch(organizeMixers());
  }, [currentSession, masterMixers, personalMixers, dispatch]);

  // Categorize mixers by group_id and dispatch to Redux
  useEffect(() => {
    if (!masterMixers || !personalMixers || masterMixers.length === 0) return;

    console.log('[useMixerHelper] Categorizing mixers by group_id');

    const metronomeTrackMixers = [];
    const backingTrackMixers = [];
    const jamTrackMixers = [];
    const recordingTrackMixers = [];
    const adhocTrackMixers = [];

    // Process master/personal mixer pairs
    for (const masterMixer of masterMixers) {
      const personalMixer = personalMixers.find(p => p.rid === masterMixer.rid);
      const mixerPair = {
        master: masterMixer,
        personal: personalMixer,
        mixer: mixMode === MIX_MODES.PERSONAL ? personalMixer : masterMixer
      };

      // Categorize by group_id (from ChannelGroupIds)
      switch (masterMixer.group_id) {
        case ChannelGroupIds.MetronomeGroup: // 16
          metronomeTrackMixers.push(mixerPair);
          console.log('[useMixerHelper] Found metronome mixer:', masterMixer.id);
          break;
        case ChannelGroupIds.BackingTrackGroup: // 11
          backingTrackMixers.push(mixerPair);
          break;
        case ChannelGroupIds.JamTrackGroup: // 12
          jamTrackMixers.push(mixerPair);
          break;
        case ChannelGroupIds.RecordingGroup: // 13
          recordingTrackMixers.push(mixerPair);
          break;
        default:
          // Other mixer types go into adhoc
          if (masterMixer.group_id !== ChannelGroupIds.UserMusicInputGroup &&
              masterMixer.group_id !== ChannelGroupIds.ChatMicGroup &&
              masterMixer.group_id !== ChannelGroupIds.BroadcastGroup) {
            adhocTrackMixers.push(mixerPair);
          }
      }
    }

    console.log('[useMixerHelper] Categorized mixers:', {
      metronome: metronomeTrackMixers.length,
      backing: backingTrackMixers.length,
      jam: jamTrackMixers.length,
      recording: recordingTrackMixers.length,
      adhoc: adhocTrackMixers.length
    });

    // Dispatch to Redux
    dispatch(setMetronomeTrackMixers(metronomeTrackMixers));
    dispatch(setBackingTrackMixers(backingTrackMixers));
    dispatch(setJamTrackMixers(jamTrackMixers));
    dispatch(setRecordingTrackMixers(recordingTrackMixers));
    dispatch(setAdhocTrackMixers(adhocTrackMixers));

  }, [masterMixers, personalMixers, mixMode, dispatch]);

  // Sync local isReady ref with Redux isReady state
  // This ensures VU meter callbacks have immediate access to the ready state
  useEffect(() => {
    isReady.current = isReadyRedux;
    if (isReadyRedux) {
      console.log('[useMixerHelper] Mixers ready, VU meters enabled');
    }
  }, [isReadyRedux]);

  // Cleanup VU state when mixers are removed
  useEffect(() => {
    // Skip if not ready or no previous mixers to compare
    if (!isReadyRedux) return;

    // Get current mixer IDs from allMixers
    const currentMixerIds = new Set(Object.keys(allMixers));

    // Track previous mixer IDs using a ref
    if (!previousMixerIdsRef.current) {
      previousMixerIdsRef.current = currentMixerIds;
      return;
    }

    // Find removed mixers (in previous but not in current)
    for (const mixerId of previousMixerIdsRef.current) {
      if (!currentMixerIds.has(mixerId)) {
        // Mixer was removed - clean up its VU state
        removeVuState(mixerId);
        console.debug('[useMixerHelper] Cleaned up VU state for removed mixer:', mixerId);
      }
    }

    // Update previous for next comparison
    previousMixerIdsRef.current = currentMixerIds;
  }, [allMixers, isReadyRedux, removeVuState]);

   const getMixerByTrackId = useCallback((trackId, mode) => {
    const mixerPair = mixersByTrackId[trackId];

    if (!mixerPair) return null;

    if (mode === undefined) {
      return mixerPair;
    } else {
      if (mode === MIX_MODES.MASTER) {
        return mixerPair.master;
      } else {
        return mixerPair.personal;
      }
    }
  }, [mixersByTrackId]);

    const groupedMixersForClientId = useCallback((clientId, groupIds, usedMixers, mixMode, currentAllMixers) => {
    const foundMixers = {};
    const mixers = mixMode === MIX_MODES.MASTER ? masterMixers : personalMixers;

    for (const mixer of mixers) {
      if (!mixer) {
        continue;
      }

      if (mixer.client_id === clientId) {
        for (const groupId of groupIds) {
          if (mixer.group_id === groupId) {
            if ((mixer.groupId !== ChannelGroupIds.UserMusicInputGroup) && !(mixer.id in usedMixers)) {
              let mixers = foundMixers[mixer.group_id];
              if (!mixers) {
                mixers = [];
                foundMixers[mixer.group_id] = mixers;
              }
              mixers.push(mixer);
            }
          }
        }
      }
    }

    return foundMixers;
  }, [masterMixers, personalMixers]);

  const findMixerForTrack = useCallback((client_id, track, myTrack, mode) => {
    let mixer = null;
    let oppositeMixer = null;
    let vuMixer = null;
    let muteMixer = null;

    if (myTrack) {
      mixer = getMixerByTrackId(track.client_track_id, mode);
      vuMixer = mixer;
      muteMixer = mixer;

      if (mixer && (mixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && mixer.group_id !== ChannelGroupIds.MidiInputMusicGroup)) {
        logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer);
      }

      if (mixer) {
        oppositeMixer = getMixerByTrackId(track.client_track_id, !mode);

        if (mode === MIX_MODES.PERSONAL) {
          muteMixer = oppositeMixer;
        }

        if (!oppositeMixer) {
          logger.error("unable to find opposite mixer for local mixer", mixer);
        } else if (oppositeMixer.group_id !== ChannelGroupIds.AudioInputMusicGroup && oppositeMixer.group_id !== ChannelGroupIds.MidiInputMusicGroup) {
          logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer);
        }
      } else {
        logger.debug("local track is not present: ", track, allMixers);
      }
    } else {
      switch (mode) {
        case MIX_MODES.MASTER:
          mixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER);

          if (mixer && (mixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && mixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup)) {
            logger.warn("master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer);
          }

          vuMixer = mixer;
          muteMixer = mixer;

          if (mixer) {
            const oppositeMixers = groupedMixersForClientId(client_id, [ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL);
            if (oppositeMixers[ChannelGroupIds.UserMusicInputGroup]) {
              oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0];
            }

            if (!oppositeMixer) {
              logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, personalMixers);
            }
          }
          break;

        case MIX_MODES.PERSONAL:
          const mixers = groupedMixersForClientId(client_id, [ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL);
          if (mixers[ChannelGroupIds.UserMusicInputGroup]) {
            mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0];

            vuMixer = mixer;
            muteMixer = mixer;

            if (mixer) {
              oppositeMixer = getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER);
              if (!oppositeMixer) {
                logger.debug("personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id);
              } else if (oppositeMixer.group_id !== ChannelGroupIds.PeerAudioInputMusicGroup && oppositeMixer.group_id !== ChannelGroupIds.PeerMidiInputMusicGroup) {
                logger.error("personal: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer);
              }
            }
          } else {
            logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers);
          }
          break;
      }
    }

    // Ensure mixer objects have the correct mode set for proper persistence
    const mixerWithMode = mixer ? { ...mixer, mode } : null;
    const oppositeMode = mode === MIX_MODES.MASTER ? MIX_MODES.PERSONAL : MIX_MODES.MASTER;
    const oppositeMixerWithMode = oppositeMixer ? { ...oppositeMixer, mode: oppositeMode } : null;
    const vuMixerWithMode = vuMixer ? { ...vuMixer, mode } : null;
    const muteMixerWithMode = muteMixer ? { ...muteMixer, mode: muteMixer === oppositeMixer ? oppositeMode : mode } : null;

    return {
      mixer: mixerWithMode,
      oppositeMixer: oppositeMixerWithMode,
      vuMixer: vuMixerWithMode,
      muteMixer: muteMixerWithMode
    };
  }, [getMixerByTrackId, groupedMixersForClientId, personalMixers, allMixers]);

  // Compute myTracks - memoized to prevent infinite re-renders
  const myTracks = useMemo(() => {
    console.debug("useMixerHelper: computing myTracks", {
      isConnected,
      currentSession,
      jamClient,
      allMixers,
      allMixersCount: allMixers ? Object.keys(allMixers).length : 0
    });

    if (!isConnected || !inSession || !jamClient || !allMixers) {
      return previousMyTracksRef.current;  // Return previous value, not []
    }

    // Safety check: if allMixers is empty during state transition, return previous value
    if (typeof allMixers === 'object' && Object.keys(allMixers).length === 0) {
      console.warn("useMixerHelper: allMixers is empty, returning previous myTracks");
      return previousMyTracksRef.current;  // Return previous value, not []
    }

    //const participant = currentSession.participants?.[jamClient.clientId];
    const participant = getParticipant(server.clientId);

    if (!participant) {
      return previousMyTracksRef.current;  // Return previous value, not []
    }

    const tracks = [];
    const connStatsClientId = participant.client_role === 'child' && participant.parent_client_id
      ? participant.parent_client_id
      : server.clientId;

      console.debug("useMixerHelper: my participant", participant, { connStatsClientId });

    const photoUrl = getAvatarUrl(participant.user.photo_url);
    const name = participant.user.name;

    // Get VST track assignments
    // First ensure VST is loaded from persistent storage (needed after page reload)
    let vstTrackAssignments = { vsts: [] };
    try {
      const isVstLoaded = jamClient.IsVstLoaded();
      const hasVstAssignment = jamClient.hasVstAssignment();
      if (hasVstAssignment && !isVstLoaded) {
        // Load VST from persistent storage - this is async but we proceed anyway
        // The UI will update on next render cycle after VST loads
        jamClient.VSTLoad();
      }
      const assignments = jamClient.VSTListTrackAssignments();
      vstTrackAssignments = assignments || { vsts: [] };
    } catch (error) {
      console.warn("Failed to get VST track assignments:", error);
    }

    const participantTracks = participant.tracks || [];
    for (let trackIndex = 0; trackIndex < participantTracks.length; trackIndex++) {
      const track = participantTracks[trackIndex];
      // Get mixers for BOTH modes to support independent Audio Mix (personal) and Session Mix (master) controls
      const masterMixerData = findMixerForTrack(participant.client_id, track, true, MIX_MODES.MASTER);
      const personalMixerData = findMixerForTrack(participant.client_id, track, true, MIX_MODES.PERSONAL);
      const mixerData = mixMode === MIX_MODES.MASTER ? masterMixerData : personalMixerData;
      const hasMixer = !!mixerData.mixer;

      const instrumentIcon = getInstrumentIcon45(track.instrument_id || track.instrument);
      const trackName = name;

      // Check if this track has a VST plugin assigned
      // Native client uses 0-based track indices for VST assignments
      const hasVst = vstTrackAssignments.vsts && Array.isArray(vstTrackAssignments.vsts)
        ? vstTrackAssignments.vsts.some(vst => vst.track === trackIndex)
        : false;

      tracks.push({
        track: {
          ...track,
          hasVst
        },
        trackIndex, // 0-based index for native client VST operations
        mixerFinder: [participant.client_id, track, true],
        mixers: mixerData,
        // Store both master and personal mixers for independent control
        masterMixers: masterMixerData,
        personalMixers: personalMixerData,
        hasMixer,
        name,
        trackName,
        instrumentIcon,
        photoUrl,
        clientId: participant.client_id,
        connStatsClientId
      });
    }

    // Store for next time
    previousMyTracksRef.current = tracks;
    return tracks;
  }, [currentSession, isConnected, jamClient, allMixers, mixMode, findMixerForTrack, getParticipant, server.clientId]);

  // useEffect(() => {
  //   console.debug("useMixerHelper: myTracks updated", myTracks);
  // }, [myTracks]);


  const mixersForGroupId = useCallback((groupId, mixMode, currentAllMixers) => {
    const foundMixers = [];
    const modePrefix = mixMode === MIX_MODES.MASTER ? 'M' : 'P';

    for (const mixerKey in currentAllMixers) {
      if (mixerKey.startsWith(modePrefix)) {
        const mixer = currentAllMixers[mixerKey];
        if (mixer && mixer.group_id === groupId) {
          foundMixers.push(mixer);
        }
      }
    }

    return foundMixers;
  }, []);

  const getGroupMixer = useCallback((categoryId, mode) => {
    const groupId = mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup;
    const oppositeGroupId = !mode === MIX_MODES.MASTER ? ChannelGroupIds.MasterCatGroup : ChannelGroupIds.MonitorCatGroup;
    const mixers = mixersForGroupId(groupId, mode, allMixers);
    const oppositeMixers = mixersForGroupId(oppositeGroupId, !mode, allMixers);

    if (mixers.length === 0) {
      return null;
    }

    let found = null;
    let oppositeFound = null;
    for (const mixer of mixers) {
      if (mixer.name === categoryId) {
        found = mixer;
        break;
      }
    }

    for (const mixer of oppositeMixers) {
      if (mixer.name === categoryId) {
        oppositeFound = mixer;
        break;
      }
    }

    if (!found) {
      logger.warn("could not find mixer with categoryId: " + categoryId);
      return null;
    } else {
      return {
        mixer: found,
        muteMixer: found,
        vuMixer: found,
        oppositeMixer: oppositeFound
      };
    }
  }, [mixersForGroupId, allMixers]);

  const mixerForGroupId = useCallback((groupId, mixMode, currentAllMixers) => {
    const mixers = mixersForGroupId(groupId, mixMode, currentAllMixers);
    if (mixers && mixers.length > 0) {
      return mixers[0];
    } else {
      return null;
    }
  }, [mixersForGroupId]);

  const getAudioInputCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.AudioInputMusic, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getChatCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.AudioInputChat, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserChatCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserChat, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getMediaCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.MediaTrack, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserMediaCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserMedia, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getUserMusicCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.UserMusic, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getMetronomeCategoryMixer = useCallback((mode, currentAllMixers) => {
    return getGroupMixer(CategoryGroupIds.Metronome, mode, currentAllMixers);
  }, [getGroupMixer]);

  const getOutputCategoryMixer = useCallback((mode, currentAllMixers) => {
    if (mode === MIX_MODES.MASTER) {
      return getGroupMixer(CategoryGroupIds.MasterCatGroup, mode, currentAllMixers);
    } else {
      return getGroupMixer(CategoryGroupIds.MonitorCatGroup, mode, currentAllMixers);
    }
  }, [getGroupMixer]);

  const getOutputMixer = useCallback((mode, currentAllMixers) => {
    if (mode === MIX_MODES.MASTER) {
      return mixerForGroupId(ChannelGroupIds.MasterGroup, mode, currentAllMixers);
    } else {
      return mixerForGroupId(ChannelGroupIds.MonitorGroup, mode, currentAllMixers);
    }
  }, [mixerForGroupId]);

  const setMixerPan = useCallback(async (mixer, panPercent) => {
    trackVolumeObject.pan = panHelpers.convertPercentToPan(panPercent);
    await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, trackVolumeObject);
  }, [trackVolumeObject, panHelpers, jamClient]);

  const getMixerByResourceId = useCallback((resourceId, mode) => {
    const mixerPair = mixersByResourceId[resourceId];

    if (!mixerPair) return null;

    if (!mode) {
      return mixerPair;
    } else {
      if (mode === MIX_MODES.MASTER) {
        return mixerPair.master;
      } else {
        return mixerPair.personal;
      }
    }
  }, [mixersByResourceId]);

  const mute = useCallback(async (mixerId, mode, muting) => {
    if (mode == null) { mode = mixMode; }

    const result = fillTrackVolumeObject(mixerId, mode);
    if (!result) return;

    const { volumeObj } = result;
    const updatedVolumeObj = { ...volumeObj, mute: muting };
    await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, updatedVolumeObj);

    const updatedMixer = getMixer(mixerId, mode);
    updatedMixer.mute = muting;
  }, [mixMode, getMixer, fillTrackVolumeObject]);

  const getOriginalVolume = useCallback((mixers, gainType) => {
    let originalVolume = null;
    if (gainType === 'music') {
      for (const mixer of mixers) {
        if (mixer.name !== CategoryGroupIds.UserMedia && mixer.name !== CategoryGroupIds.MediaTrack) {
          originalVolume = mixer.volume_left;
          break;
        }
      }
    } else {
      originalVolume = mixers[0].volume_left;
    }

    return originalVolume;
  }, []);

  const faderChanged = useCallback(async (data, mixers, gainType, controlGroup) => {
    //console.log("MixerHelper: faderChanged called", { data, mixers, gainType, controlGroup });
    if (!Array.isArray(mixers)) {
      mixers = [mixers];
    }
    const originalVolume = getOriginalVolume(mixers, gainType);


    // Handle multiple mixers (master + personal pairs like web version)
    const mixerIds = mixers.map(m => m.id);
    const hasMasterAndPersonalControls = mixerIds.length === 2;

    for (let i = 0; i < mixers.length; i++) {
      const m = mixers[i];

      // Broadcast only when NOT dragging (matches web version logic)
      const broadcast = !(data.dragging);

      // Determine mode for multiple mixers (like web version)
      let mode = m.mode;
      if (hasMasterAndPersonalControls) {
        mode = i === 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL;
      }

      const result = fillTrackVolumeObject(m.id, mode, allMixers, broadcast);
      if (result == null) {
        console.error("MixerHelper: faderChanged: mixer is null, skipping", m, gainType, controlGroup);
        continue;
      }

      const { mixer, volumeObj } = result;

      // Handle relative volume adjustments for music category (matches web version)
      const relative = gainType === 'music' && (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack);

      await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, volumeObj);

      // Redux: Update local mixer state via dispatch
      const newVolume = faderHelpers.convertPercentToAudioTaper(data.percentage);
      dispatch(updateMixer({
        mixerId: mixer.id,
        mode: mixer.mode,
        updates: {
          volume_left: newVolume
        }
      }));
    }
  }, [getOriginalVolume, getMixer, fillTrackVolumeObject, setMixerVolume, allMixers, trackVolumeObject, dispatch]);

  const initGain = useCallback((mixer) => {
    if (Array.isArray(mixer)) {
      mixer = mixer[0];
    }

    // In React version, fader initialization is handled through component state
    // This function is kept for API compatibility with web version
    // No DOM manipulation needed since React handles state updates
    console.debug('initGain called for mixer:', mixer?.id);
  }, []);

  const panChanged = useCallback(async (data, mixers, groupId) => {
    if (!Array.isArray(mixers)) { mixers = [mixers]; }
    const result = [];
    for (const mixer of mixers) {
      const broadcast = !(data.dragging);
      const fillResult = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast);
      if (!fillResult) continue;

      const { mixer: filledMixer } = fillResult;
      await setMixerPan(filledMixer, data.percentage);

      const updatedMixer = getMixer(filledMixer.id, filledMixer.mode, allMixers);
      updatedMixer.pan = trackVolumeObject.pan;
      result.push(trackVolumeObject.pan);
    }
    return result;
  }, [getMixer, fillTrackVolumeObject, setMixerPan, allMixers, trackVolumeObject]);

  const initPan = useCallback((mixer) => {
    const panPercent = panHelpers.convertPanToPercent(mixer.pan);
    faderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan));
    faderHelpers.showFader(mixer.id);
  }, [faderHelpers, panHelpers]);

  const loopChanged = useCallback(async (mixer, shouldLoop) => {
    const result = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers);
    if (!result) return;

    const { volumeObj } = result;
    const updatedVolumeObj = { ...volumeObj, loop: shouldLoop };

    setTrackVolumeObject(updatedVolumeObj);
    await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedVolumeObj);

    const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers);
    updatedMixer.loop = shouldLoop;
  }, [getMixer, fillTrackVolumeObject, allMixers, setTrackVolumeObject]);

  const percentFromMixerValue = useCallback((min, max, value) => {
    try {
      const range = Math.abs(max - min);
      const magnitude = value - min;
      const percent = Math.round(100 * (magnitude / range));
      return percent;
    } catch (err) {
      return 0;
    }
  }, []);

  const percentToMixerValue = useCallback((min, max, percent) => {
    const range = Math.abs(max - min);
    const multiplier = percent / 100;
    let value = min + (multiplier * range);

    if (value < min) {
      value = min;
    }

    if (value > max) {
      value = max;
    }

    return value;
  }, []);

  const collectStats = useCallback((mixer, currentVuStats) => {
    let mixerStats = currentVuStats[mixer.id];

    if (!mixerStats) {
      mixerStats = { count: 0, group_name: context.JK.groupIdDisplay(mixer) };
      currentVuStats[mixer.id] = mixerStats;
    }

    mixerStats.count++;
  }, []);

  const dumpVUStats = useCallback((currentVuStats) => {
    logger.debug("VU STAT DUMP");
    for (const mixerId in currentVuStats) {
      const mixerStat = currentVuStats[mixerId];
      logger.debug("VU STAT: #{mixerStat.group_name} count=#{mixerStat.count}");
    }
  }, []);

  // Change updateVU to use the ref
  const updateVU = useCallback((mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) => {
    if (!isReady.current) {
      return;
    }

    const mixer = getMixer(mixerId, mode);

    if (mixer) {
      updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping);
    }
  }, [getMixer, updateVU3]);

  const getTrackInfo = useCallback(async () => {
    return await context.JK.TrackHelpers.getTrackInfo(context.jamClient, masterMixers);
  }, [masterMixers]);

  const mixersForGroupIds = useCallback((groupIds, mixMode, currentAllMixers) => {
    const foundMixers = [];
    const modePrefix = mixMode === MIX_MODES.MASTER ? 'M' : 'P';

    for (const mixerKey in currentAllMixers) {
      if (mixerKey.startsWith(modePrefix)) {
        const mixer = currentAllMixers[mixerKey];
        if (mixer && groupIds.includes(mixer.group_id)) {
          foundMixers.push(mixer);
        }
      }
    }

    return foundMixers;
  }, []);

  const refreshMixer = useCallback((mixers, currentAllMixers) => {
    if (!mixers || !mixers.mixer) return null;

    let updateMixers = null;
    if (Array.isArray(mixers.mixer)) {
      if (mixers.mixer.length > 0) {
        updateMixers = [];
        for (const mixer of mixers.mixer) {
          updateMixers.push(getMixer(mixer.id, mixer.mode, currentAllMixers));
        }
      }
    } else {
      updateMixers = getMixer(mixers.mixer.id, mixers.mixer.mode, currentAllMixers);
    }

    let updatedVUMixers = null;
    if (Array.isArray(mixers.vuMixer)) {
      updatedVUMixers = [];
      for (const vuMixer of mixers.vuMixer) {
        updatedVUMixers.push(getMixer(vuMixer.id, vuMixer.mode, currentAllMixers));
      }
    } else {
      updatedVUMixers = getMixer(mixers.vuMixer.id, mixers.vuMixer.mode, currentAllMixers);
    }

    let updateMuteMixers = null;
    if (Array.isArray(mixers.muteMixer)) {
      updateMuteMixers = [];
      for (const muteMixer of mixers.muteMixer) {
        updateMuteMixers.push(getMixer(muteMixer.id, muteMixer.mode, currentAllMixers));
      }
    } else {
      updateMuteMixers = getMixer(mixers.muteMixer.id, mixers.muteMixer.mode, currentAllMixers);
    }

    const oppositeMixer = mixers.oppositeMixer ? getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode, currentAllMixers) : null;

    if (updateMixers) {
      return {
        mixer: updateMixers,
        vuMixer: updatedVUMixers,
        muteMixer: updateMuteMixers,
        oppositeMixer: oppositeMixer
      };
    } else {
      return null;
    }
  }, [getMixer]);

  const recordingName = useCallback(() => {
    return currentSession.recordingName();
  }, [currentSession]);

  const jamTrackName = useCallback(() => {
    return currentSession.jamTrackName();
  }, [currentSession]);


  return {
    isReady,
    session: currentSession, // Return currentSession from Redux instead of local state
    // masterMixers,
    // personalMixers,
    mixers: allMixers,
    myTracks,
    findMixerForTrack,
    updateMixerData,
    updateVU,
    simulatedMusicCategoryMixers,
    simulatedChatCategoryMixers,
    mixMode,
    faderChanged,
    initGain,
    setMixerPan,
    // Category mixer functions for Volume modal
    getAudioInputCategoryMixer,
    getChatCategoryMixer,
    // Media arrays from mediaSlice
    backingTracks,
    jamTracks,
    recordedTracks
  };
}

export default useMixerHelper;
