import { useState, useCallback, useRef, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { debounce } from 'lodash'; // Add lodash for debouncing
import { useJamClient } from '../context/JamClientContext';
import {
  selectActiveSession,
  selectSessionId,
  selectInSession,
  setSessionId,
  updateSessionData
} from '../store/features/activeSessionSlice';
import {
  setBackingTracks,
  setJamTracks,
  setRecordedTracks
} from '../store/features/mediaSlice';
import useGearUtils from './useGearUtils';
import useTrackHelpers from './useTrackHelpers';
import useRecordingHelpers from './useRecordingHelpers';
import useSessionUtils from './useSessionUtils';
import { joinSession as joinSessionRest, getSession, deleteParticipant } from '../helpers/rest';
import { MessageType } from '../helpers/MessageFactory';
import { LATENCY, SESSION_PRIVACY_MAP } from '../helpers/globals';

const logger = console;

// Constants from original sessionModelAsync.js
const ALERT_TYPES = {
  // Add alert types as needed
};

const EVENTS = {
  MIXER_MODE_CHANGED: 'mixer_mode_changed',
  SESSION_STARTED: 'session_started',
  SESSION_ENDED: 'session_ended'
};

const MIX_MODES = {
  PERSONAL: 'personal',
  MASTER: 'master'
};

export default function useSessionModel(app, server, sessionScreen) {
  const jamClient = useJamClient();
  const { isNoInputProfile } = useGearUtils();
  const { getTrackInfo, getUserTracks } = useTrackHelpers();
  const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers();

  // Phase 4: Replace CurrentSessionContext with Redux
  const dispatch = useDispatch();
  const currentSession = useSelector(selectActiveSession);
  const sessionId = useSelector(selectSessionId);
  const inSessionFlag = useSelector(selectInSession);

  // Create ref for high-frequency access (performance optimization)
  const sessionIdRef = useRef(sessionId);
  useEffect(() => {
    sessionIdRef.current = sessionId;
  }, [sessionId]);

  // Maintain function interface for backward compatibility
  const currentSessionIdRef = sessionIdRef;

  const inSession = useCallback(() => {
    return sessionIdRef.current !== null;
  }, []);

  const setCurrentSessionId = useCallback((id) => {
    console.log("Setting current session ID to: ", id);
    dispatch(setSessionId(id));
  }, [dispatch]);

  const setCurrentSession = useCallback((updater) => {
    if (typeof updater === 'function') {
      // Handle functional setState pattern
      const currentData = currentSession;
      const newData = updater(currentData);
      dispatch(updateSessionData(newData));
    } else {
      dispatch(updateSessionData(updater));
    }
  }, [dispatch, currentSession]);

  const history = useHistory();

  // State variables from original SessionModel
  const [userTracks, setUserTracks] = useState(null);
  const [currentOrLastSession, setCurrentOrLastSession] = useState(null);
  const [subscribers, setSubscribers] = useState({});
  const [users, setUsers] = useState({}); // User info for session participants
  const [requestingSessionRefresh, setRequestingSessionRefresh] = useState(false);
  const [pendingSessionRefresh, setPendingSessionRefresh] = useState(false);
  const [currentTrackChanges, setCurrentTrackChanges] = useState(0);
  const [participantsEverSeen, setParticipantsEverSeen] = useState({});
  const [currentParticipants, setCurrentParticipants] = useState({});
  const [sessionPageEnterDeferred, setSessionPageEnterDeferred] = useState(null);
  const [sessionPageEnterTimeout, setSessionPageEnterTimeout] = useState(null);
  const [startTime, setStartTime] = useState(null);
  const [joinDeferred, setJoinDeferred] = useState(null);
  const [previousAllTracks, setPreviousAllTracks] = useState({
    userTracks: [],
    backingTracks: [],
    metronomeTracks: []
  });
  const [openBackingTrack, setOpenBackingTrack] = useState(null);
  const [shownAudioMediaMixerHelp, setShownAudioMediaMixerHelp] = useState(false);
  const [controlsLockedForJamTrackRecording, setControlsLockedForJamTrackRecording] = useState(false);
  const [mixerMode, setMixerMode] = useState(MIX_MODES.PERSONAL);

  // Refs for backend alert throttling
  const backendMixerAlertThrottleTimerRef = useRef(null);

  // State to hold pending promises and their resolvers (from useSessionEnter)
  const pendingPromisesRef = useRef(new Map());

  // Refs for session leave functionality (from useSessionLeave)
  const isLeavingRef = useRef(false);
  const recordingModelRef = useRef(null);

  // Client ID - would need to be passed or obtained from context
  const clientId = jamClient?.clientID || 'unknown';

  // Promise management functions (from useSessionEnter)
  const resolvePendingPromises = useCallback((inputTracks) => {
    for (const { resolve } of pendingPromisesRef.current.values()) {
      resolve(inputTracks);
    }
    pendingPromisesRef.current.clear();
  }, []);

  const rejectPendingPromises = useCallback((reason) => {
    for (const { reject } of pendingPromisesRef.current.values()) {
      reject(reason);
    }
    pendingPromisesRef.current.clear();
  }, []);

  // Event handlers (from useSessionEnter)
  const onWatchedInputs = useCallback((inputTracks) => {
    resolvePendingPromises(inputTracks);
  }, [resolvePendingPromises]);

  const onMixersChanged = useCallback((type, text, trackInfo) => {
    if (text === 'RebuildAudioIoControl' && trackInfo.userTracks.length > 0) {
      resolvePendingPromises(trackInfo.userTracks);
    }
  }, [resolvePendingPromises]);

  // Initialize session model
  // useEffect(() => {
  //   if (server) {
  //     server.registerOnSocketClosed(onWebsocketDisconnected);
  //   }

  //   return () => {
  //     if (server) {
  //       server.unregisterOnSocketClosed(onWebsocketDisconnected);
  //     }
  //   };
  // }, [server]);

  // Cleanup on unmount (from useSessionEnter)
  useEffect(() => {
    return () => {
      rejectPendingPromises('component_unmounted');
    };
  }, [rejectPendingPromises]);

  // Cleanup backend mixer alert throttle timer on unmount
  useEffect(() => {
    return () => {
      if (backendMixerAlertThrottleTimerRef.current) {
        clearTimeout(backendMixerAlertThrottleTimerRef.current);
        backendMixerAlertThrottleTimerRef.current = null;
      }
    };
  }, []);

  // Core session functions
  const id = useCallback(() => {
    return currentSessionIdRef.current;
  }, [currentSessionIdRef]);

  const start = useCallback((sessionId) => {
    setCurrentSessionId(sessionId);
    setStartTime(new Date().getTime());
  }, [setCurrentSessionId]);

  const setUserTracksState = useCallback((_userTracks) => {
    setUserTracks(_userTracks);
  }, []);

  const participants = useCallback(() => {
    if (currentSession) {
      return currentSession.participants || [];
    }
    return [];
  }, [currentSession]);

  // Check if metronome is open
  const isMetronomeOpen = useCallback(() => {
    let metronomeOpen = false;
    participants().forEach((participant) => {
      if (participant.metronome_open) {
        metronomeOpen = true;
      }
    });
    return metronomeOpen;
  }, [participants]);

  // Check if playing recording
  const isPlayingRecording = useCallback(() => {
    return !!(currentSession && currentSession.claimed_recording);
  }, [currentSession]);

  // Get recorded tracks
  const recordedTracks = useCallback(() => {
    if (currentSession && currentSession.claimed_recording) {
      return currentSession.claimed_recording.recording.recorded_tracks;
    }
    return null;
  }, [currentSession]);

  // Get recorded backing tracks
  const recordedBackingTracks = useCallback(() => {
    if (currentSession && currentSession.claimed_recording) {
      return currentSession.claimed_recording.recording.recorded_backing_tracks;
    }
    return null;
  }, [currentSession]);

  // Get backing tracks
  const backingTracks = useCallback(() => {
    let backingTracks = [];
    participants().forEach((participant) => {
      if (participant.backing_tracks && participant.backing_tracks.length > 0) {
        backingTracks = participant.backing_tracks;
      }
    });
    return backingTracks;
  }, [participants]);

  // Get jam tracks
  const jamTracks = useCallback(() => {
    if (currentSession && currentSession.jam_track) {
      return currentSession.jam_track.tracks.filter(track => track.track_type === 'Track');
    }
    return null;
  }, [currentSession]);

  // Get recorded jam tracks
  const recordedJamTracks = useCallback(() => {
    if (currentSession && currentSession.claimed_recording) {
      return currentSession.claimed_recording.recording.recorded_jam_track_tracks;
    }
    return null;
  }, [currentSession]);

  // Get jam track name
  const jamTrackName = useCallback(() => {
    if (currentSession && currentSession.jam_track) {
      return currentSession.jam_track.name;
    }
    return null;
  }, [currentSession]);

  // Get recorded jam track name
  const recordedJamTrackName = useCallback(() => {
    if (currentSession && currentSession.claimed_recording && currentSession.claimed_recording.recording.jam_track) {
      return currentSession.claimed_recording.recording.jam_track.name;
    }
    return null;
  }, [currentSession]);

  // Check if self opened jam tracks
  const selfOpenedJamTracks = useCallback(() => {
    return currentSession && (currentSession.jam_track_initiator_id === window.JK?.currentUserId);
  }, [currentSession]);

  // Get backing track
  const backingTrack = useCallback(() => {
    if (currentSession) {
      return {
        path: currentSession.backing_track_path
      };
    }
    return null;
  }, [currentSession]);

  // Get creator ID
  const creatorId = useCallback(() => {
    if (!currentSession) {
      throw "creator is not known";
    }
    return currentSession.user_id;
  }, [currentSession]);

  // Get musician access
  const getMusicianAccess = useCallback(() => {
    if (!currentSession.musician_access && !currentSession.approval_required) {
      return SESSION_PRIVACY_MAP.private_invite;
    } else if (currentSession.musician_access && currentSession.approval_required) {
      return SESSION_PRIVACY_MAP.private_approve;
    } else if (currentSession.musician_access && !currentSession.approval_required) {
      return SESSION_PRIVACY_MAP.public;
    }

  }, [currentSession]);

  // Check if already in session
  const alreadyInSession = useCallback(() => {
    let inSession = false;
    participants().forEach((participant) => {
      if (participant.user.id === window.JK?.currentUserId) {
        inSession = true;
      }
    });
    return inSession;
  }, [participants]);

  // Control locking functions
  const lockControlsforJamTrackRecording = useCallback(() => {
    setControlsLockedForJamTrackRecording(true);
  }, []);

  const unlockControlsforJamTrackRecording = useCallback(() => {
    setControlsLockedForJamTrackRecording(false);
  }, []);

  const areControlsLockedForJamTrackRecording = useCallback(() => {
    return controlsLockedForJamTrackRecording;
  }, []);

  // Mixer mode functions
  const onMixerModeChanged = useCallback((newMixerMode) => {
    setMixerMode(newMixerMode);
    const mode = newMixerMode === MIX_MODES.MASTER ? "master" : "personal";
    logger.debug("onMixerModeChanged:" + mode);
    // Trigger event - in React this would be through state updates or callbacks
  }, []);

  const setMixerModeState = useCallback((newMixerMode) => {
    if (mixerMode !== newMixerMode) {
      onMixerModeChanged(newMixerMode);
    }
  }, [mixerMode, onMixerModeChanged]);

  const isMasterMixMode = useCallback(() => {
    return mixerMode === MIX_MODES.MASTER;
  }, [mixerMode]);

  const isPersonalMixMode = useCallback(() => {
    return mixerMode === MIX_MODES.PERSONAL;
  }, [mixerMode]);

  const getMixMode = useCallback(() => {
    return mixerMode;
  }, [mixerMode]);

  // Wait for session page enter done (promise-based from useSessionEnter)
  const waitForSessionPageEnterDone = useCallback(() => {
    return new Promise(async (resolve, reject) => {
      // Check if tracks are already available
      const inputTracks = await getUserTracks();

      logger.debug("isNoInputProfile", await isNoInputProfile());
      logger.debug("inputTracks", inputTracks);
      if (inputTracks.length > 0 || await isNoInputProfile()) {
        logger.debug("on page enter, tracks are already available");
        resolve(inputTracks);
        return;
      }

      // Create timeout promise
      const timeoutPromise = new Promise((_, rejectTimeout) => {
        setTimeout(() => rejectTimeout('timeout'), 5000);
      });

      // Create promise that waits for tracks
      const tracksPromise = new Promise((resolveTracks, rejectTracks) => {
        const promiseId = Symbol('sessionPageEnter');
        pendingPromisesRef.current.set(promiseId, { resolve: resolveTracks, reject: rejectTracks });

        // Cleanup function to remove this promise if it gets resolved/rejected elsewhere
        return () => {
          pendingPromisesRef.current.delete(promiseId);
        };
      });

      // Race between tracks and timeout
      Promise.race([tracksPromise, timeoutPromise])
        .then(resolve)
        .catch(reject);
    });
  }, [getUserTracks, isNoInputProfile]);

  // Duplicate declaration of SessionPageEnter removed to fix redeclaration error.


  // Join session
  const joinSession = useCallback(async (sessionId) => {
    logger.debug("SessionModel.joinSession(" + sessionId + ")");

    const deferred = joinSessionRest({ session_id: sessionId });
    setJoinDeferred(deferred);

    try {
      const response = await deferred;

      if (!inSession()) {
        logger.debug("user left before fully joined to session. telling server again that they have left");
        // leaveSessionRest(response.id); // Would need to implement
        return;
      }

      logger.debug("calling jamClient.JoinSession");
      if (!alreadyInSession()) {
        // GA tracking would go here
      }

      resetRecordingState(currentSessionIdRef.current);
      // Register message callbacks would go here

      // Trigger session started event
      // $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: sessionId}});

      refreshCurrentSession(true);
    } catch (error) {
      updateCurrentSession(null);
    }

    return deferred;
  }, [currentSessionIdRef]);

  // Set recording model (from useSessionLeave)
  const setRecordingModel = useCallback((recordingModel) => {
    recordingModelRef.current = recordingModel;
  }, []);

  // Leave session REST call (from useSessionLeave)
  const leaveSessionRest = useCallback(async () => {
    const clientId = await jamClient.GetClientID();
    if (!clientId) return;

    try {
      await deleteParticipant(clientId);
      logger.debug("Successfully left session via REST API");
    } catch (error) {
      logger.error("Error leaving session via REST:", error);
      // Don't throw - we want to continue with client-side cleanup
    }
  }, [jamClient, logger]);

  // Perform the actual leave session (from useSessionLeave)
  const performLeaveSession = useCallback(async () => {
    
    if (isLeavingRef.current) {
      logger.debug("Leave session already in progress");
      return;
    }

    isLeavingRef.current = true;
    const sessionId = currentSessionIdRef.current;

    try {
      logger.debug("Starting session leave process");

      // Stop recording if needed
      if (recordingModelRef.current?.stopRecordingIfNeeded) {
        try {
          await recordingModelRef.current.stopRecordingIfNeeded();
          logger.debug("Recording stopped successfully");
        } catch (error) {
          logger.error("Error stopping recording:", error);
        }
      }

      

      // Leave the session via jamClient (don't wait for REST)
      logger.debug("Leaving session via jamClient for sessionId:", sessionId);
      try {
        await jamClient.LeaveSession({ sessionID: sessionId });
        logger.debug("Successfully left session via jamClient");
      } catch (error) {
        logger.error("Error leaving session via jamClient:", error);
      }

      // Make REST call to server (fire and forget for faster UX)
      await leaveSessionRest(sessionId);

      // Unregister callbacks
      try {
        await jamClient.SessionRegisterCallback("");
        await jamClient.SessionSetAlertCallback("");
        await jamClient.SessionSetConnectionStatusRefreshRate(0);
        logger.debug("Callbacks unregistered successfully");
      } catch (error) {
        logger.error("Error unregistering callbacks:", error);
      }

      

      // Call session page leave
      try {
        // Note: SessionPageLeave would need to be imported from useSessionUtils
        // For now, we'll skip this as it's not directly available
        logger.debug("Session page leave completed");
      } catch (error) {
        logger.error("Error in SessionPageLeave:", error);
      }

      // Trigger session ended event
      // TODO: Re-implement this with React context or state management
      // if (document && window.$) {
      //   // $(document).trigger('SESSION_ENDED', { session: { id: sessionId } });
      //   logger.debug("Session ended event triggered");
      // }

      logger.debug("Session leave completed successfully");
      

    } catch (error) {
      logger.error("Unexpected error during session leave:", error);
      throw error;
    } finally {
      // Reset session state
      currentSessionIdRef.current = null;
      recordingModelRef.current = null;
      isLeavingRef.current = false;
    }
  }, [jamClient, leaveSessionRest, logger]);

  // Main leave session function (from useSessionLeave)
  const leaveSession = useCallback(async () => {
    await performLeaveSession();
  }, [performLeaveSession]);

  // Handle leave session with behavior (navigation, notifications, etc.) (from useSessionLeave)
  const handleLeaveSession = useCallback(async (behavior = {}) => {
    logger.debug("Handling leave session with behavior:", behavior);

    try {
      // Handle notifications
      // if (behavior.notify && window.JK?.app?.layout) {
      //   window.JK.app.layout.notify(behavior.notify);
      // }

      // // Allow leave session (trigger any leave confirmation logic)
      // if (window.SessionActions?.allowLeaveSession) {
      //   window.SessionActions.allowLeaveSession.trigger();
      // }

      // Perform the leave operation
      await leaveSession();

      // Handle navigation after successful leave
      // if (behavior.location) {
      //   if (typeof behavior.location === 'number') {
      //     window.history.go(behavior.location);
      //   } else {
      //     window.location = behavior.location;
      //   }
      // } else if (behavior.hash) {
      //   window.location.hash = behavior.hash;
      // } else {
      //   logger.warn("No location specified in leaveSession action, defaulting to home", behavior);
      //   window.location = '/client#/home';
      // }

      // Handle lesson session rating if applicable
      // Note: This would need additional context/state to implement fully
      // For now, just log that rating logic would go here
      //logger.debug("Lesson session rating logic would be handled here");

      

    } catch (error) {
      logger.error("Error handling leave session:", error);
      throw error;
    }
  }, [leaveSession, logger]);

  // Check if currently leaving (from useSessionLeave)
  const isLeaving = useCallback(() => {
    return isLeavingRef.current;
  }, []);

  // Leave current session (legacy function, now delegates to leaveSession)
  const leaveCurrentSession = useCallback(async () => {
    return leaveSession();
  }, [leaveSession]);

  // Refresh current session
  const refreshCurrentSession = useCallback(async (force = false) => {
    if (force) {
      logger.debug("refreshCurrentSession(force=true)");
    }

    await refreshCurrentSessionRest(sessionChanged, force);
  }, []);

  // Track changes handler - debounced to prevent excessive session refreshes
  const trackChanges = useCallback(debounce((header, payload) => {
    if (currentTrackChanges < payload.track_changes_counter) {
      logger.debug("track_changes_counter = stale. refreshing...");
      refreshCurrentSession();
    } else {
      if (header.type !== 'HEARTBEAT_ACK') {
        logger.info("track_changes_counter = fresh. skipping refresh...", header, payload);
      }
    }
  }, 500), [currentTrackChanges, refreshCurrentSession]);

  // Subscribe to session changes
  const subscribe = useCallback((subscriberId, sessionChangedCallback) => {
    logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])");
    setSubscribers(prev => ({
      ...prev,
      [subscriberId]: sessionChangedCallback
    }));
  }, []);

  // Notify subscribers of session changes
  const sessionChanged = useCallback(() => {
    Object.values(subscribers).forEach(callback => {
      callback(currentSession);
    });
  }, [subscribers, currentSession]);

  // Session ended cleanup
  const sessionEnded = useCallback((fullyJoined) => {
    // Cleanup logic
    setUserTracks(null);
    setStartTime(null);
    setJoinDeferred(null);
    setMixerMode(MIX_MODES.PERSONAL);

    if (sessionPageEnterDeferred) {
      // sessionPageEnterDeferred.reject('session_over');
      setSessionPageEnterDeferred(null);
    }

    setCurrentParticipants({});
    setPreviousAllTracks({
      userTracks: [],
      backingTracks: [],
      metronomeTracks: []
    });
    setOpenBackingTrack(null);
    setShownAudioMediaMixerHelp(false);
    setControlsLockedForJamTrackRecording(false);

    if (fullyJoined) {
      // $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: currentSessionIdRef.current}});
    }
    setCurrentSessionId(null);
  }, [sessionPageEnterDeferred]);

  // Update current session
  const updateCurrentSession = useCallback((sessionData) => {
    if (sessionData !== null) {
      setCurrentOrLastSession(sessionData);
    }

    const beforeUpdate = currentSession;
    setCurrentSession(sessionData);

    if (sessionData === null) {
      sessionEnded(beforeUpdate !== null);
    }
  }, [currentSession, sessionEnded, setCurrentSession]);

  // Update session info
  const updateSessionInfo = useCallback((response, callback, force) => {
    if (force === true || currentTrackChanges < response.track_changes_counter) {
      logger.debug("updating current track changes from %o to %o", currentTrackChanges, response.track_changes_counter);
      setCurrentTrackChanges(response.track_changes_counter);

      // sendClientParticipantChanges logic would go here

      updateCurrentSession(response);

      // Extract and dispatch media arrays to Redux
      // Extract backing tracks from participants
      let extractedBackingTracks = [];
      if (response.participants) {
        response.participants.forEach((participant) => {
          if (participant.backing_tracks && participant.backing_tracks.length > 0) {
            extractedBackingTracks = participant.backing_tracks;
          }
        });
      }
      // Only update backing tracks if server has data
      // Avoids race condition: local state set from native client gets cleared
      // by session refresh before server sync completes
      if (extractedBackingTracks.length > 0) {
        dispatch(setBackingTracks(extractedBackingTracks));
        console.log('[useSessionModel] Extracted backing tracks:', extractedBackingTracks);
      }

      // Extract jam tracks
      const extractedJamTracks = response.jam_track && response.jam_track.tracks
        ? response.jam_track.tracks.filter(track => track.track_type === 'Track')
        : [];
      dispatch(setJamTracks(extractedJamTracks));
      console.log('[useSessionModel] Extracted jam tracks:', extractedJamTracks);

      // Extract recorded tracks
      const extractedRecordedTracks = response.claimed_recording && response.claimed_recording.recording
        ? response.claimed_recording.recording.recorded_tracks || []
        : [];
      dispatch(setRecordedTracks(extractedRecordedTracks));
      console.log('[useSessionModel] Extracted recorded tracks:', extractedRecordedTracks);

      if (callback) {
        callback();
      }
    } else {
      logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
    }
  }, [currentTrackChanges, updateCurrentSession, dispatch]);

  // Refresh current session REST call
  const refreshCurrentSessionRest = useCallback(async (callback, force) => {
    if (!inSession()) {
      logger.debug("refreshCurrentSession skipped: ");
      return;
    }

    if (requestingSessionRefresh) {
      logger.debug("queueing refresh");
      setPendingSessionRefresh(true);
      return;
    }

    setRequestingSessionRefresh(true);

    try {
      const response = await getSession(currentSessionIdRef.current);
      const data = await response.json();
      updateSessionInfo(data, callback, force);
    } catch (jqXHR) {
      if (jqXHR.status !== 404) {
        app.notifyServerError(jqXHR, "Unable to refresh session data");
      } else {
        logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone");
      }
    } finally {
      setRequestingSessionRefresh(false);
      if (pendingSessionRefresh) {
        setPendingSessionRefresh(false);
        refreshCurrentSessionRest(sessionChanged, force);
      }
    }
  }, [inSession, requestingSessionRefresh, pendingSessionRefresh, currentSessionIdRef, updateSessionInfo, sessionChanged, app]);

  // Participant management functions
  const participantForClientId = useCallback((clientId) => {
    const foundParticipant = participants().find(participant => participant.client_id === clientId);
    return foundParticipant;
  }, [participants]);

  // Track sync functionality
  const syncTracks = useCallback(async (allTracks) => {
    if (!inSession()) {
      logger.debug("dropping queued up sync tracks because no longer in session");
      return null;
    }

    if (!allTracks) {
      allTracks = await getTrackInfo();
    }

    const inputTracks = allTracks.userTracks;
    const backingTracksData = allTracks.backingTracks;
    const metronomeTracks = allTracks.metronomeTracks;

    const syncTrackRequest = {
      client_id: clientId,
      tracks: inputTracks,
      backing_tracks: backingTracksData,
      metronome_open: metronomeTracks.length > 0,
      id: id()
    };

    // REST call would go here
    // return rest.putTrackSyncChange(syncTrackRequest);
    return Promise.resolve();
  }, [inSession, getTrackInfo, clientId, id]);

  // WebSocket disconnected handler
  const onWebsocketDisconnected = useCallback(async (in_error) => {
    if (currentSessionIdRef.current) {
      logger.debug("onWebsocketDisconnect: calling jamClient.LeaveSession for clientId=" + clientId);
      await jamClient.LeaveSession({ sessionID: currentSessionIdRef.current });
    }
  }, [jamClient, currentSessionIdRef, clientId]);

  // Find user by criteria
  const findUserBy = useCallback((finder) => {
    if (finder.clientId) {
      const foundParticipant = participants().find(participant => participant.client_id === finder.clientId);
      if (foundParticipant) {
        return Promise.resolve(foundParticipant.user);
      }
    }
    return Promise.reject();
  }, [participants]);

  // Alert handlers
  const onDeadUserRemove = useCallback((type, text) => {
    if (!inSession()) return;
    const clientId = text;
    const participant = participantsEverSeen[clientId];
    if (participant) {
      app.notify({
        "title": ALERT_TYPES[type]?.title || "User Issue",
        "text": participant.user.name + " is no longer sending audio.",
        "icon_url": "" // avatar URL
      });
      // Track disabling logic would go here
    }
  }, [inSession, participantsEverSeen, app]);

  const onWindowBackgrounded = useCallback((type, text) => {
    // Window backgrounded logic
  }, []);

  const onBroadcastSuccess = useCallback((type, text) => {
    // Broadcast success logic
  }, []);

  const onBroadcastFailure = useCallback((type, text) => {
    // Broadcast failure logic
  }, []);

  const onBroadcastStopped = useCallback((type, text) => {
    // Broadcast stopped logic
  }, []);

  const onPlaybackStateChange = useCallback((type, text) => {
    if (sessionScreen) {
      sessionScreen.onPlaybackStateChange(text);
    }
  }, [sessionScreen]);

  const onBackendMixerChanged = useCallback(async (type, text) => {
    logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);

    if (inSession() && text === "RebuildAudioIoControl") {
      if (backendMixerAlertThrottleTimerRef.current) {
        clearTimeout(backendMixerAlertThrottleTimerRef.current);
      }

      backendMixerAlertThrottleTimerRef.current = setTimeout(async () => {
        // Track availability check logic
        if (joinDeferred) {
          joinDeferred.done(() => {
            syncTracks();
          });
        }
      }, 100);
    } else if (inSession() && (text === 'RebuildMediaControl' || text === 'RebuildRemoteUserControl')) {
      const allTracks = await getTrackInfo();
      const backingTracksData = allTracks.backingTracks;
      const previousBackingTracks = previousAllTracks.backingTracks;
      const metronomeTracks = allTracks.metronomeTracks;
      const previousMetronomeTracks = previousAllTracks.metronomeTracks;

      if (!(previousBackingTracks.length === 0 && backingTracksData.length === 0) &&
        JSON.stringify(previousBackingTracks) !== JSON.stringify(backingTracksData)) {
        logger.debug("backing tracks changed", previousBackingTracks, backingTracksData);
        await syncTracks(allTracks);
        // Refresh session to get updated mixer data for backing tracks
        refreshCurrentSession(true);
      } else if (!(previousMetronomeTracks.length === 0 && metronomeTracks.length === 0) &&
        JSON.stringify(previousMetronomeTracks) !== JSON.stringify(metronomeTracks)) {
        logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks);
        await syncTracks(allTracks);
        // Refresh session to get updated mixer data
        refreshCurrentSession(true);
      } else {
        refreshCurrentSession(true);
      }

      setPreviousAllTracks(allTracks);
    } else if (inSession() && (text === 'Global Peer Input Mixer Mode')) {
      setMixerModeState(MIX_MODES.MASTER);
    } else if (inSession() && (text === 'Local Peer Stream Mixer Mode')) {
      setMixerModeState(MIX_MODES.PERSONAL);
    }
  }, [inSession, joinDeferred, syncTracks, getTrackInfo, previousAllTracks, refreshCurrentSession, setMixerModeState]);

  // Audio establishment tracking
  const setAudioEstablished = useCallback((clientId, audioEstablished) => {
    setCurrentParticipants(prev => {
      const participant = prev[clientId];
      if (participant) {
        if (participant.client.audio_established === null) {
          participant.client.audio_established = audioEstablished;
          // Stats tracking would go here
        } else if (participant.client.audio_established === false && audioEstablished === true) {
          // Delayed success tracking
        }
      }
      return prev;
    });
  }, []);

  // Clear audio timeout (from useSessionUtils)
  const clearAudioTimeout = useCallback(() => {
    if (window.JK?.AudioStopTimeout) {
      clearTimeout(window.JK.AudioStopTimeout);
      window.JK.AudioStopTimeout = null;
    }
  }, []);

  // FTUE functions (from useSessionUtils)
  const FTUEPageEnter = useCallback(async () => {
    logger.debug("sessionUtils: FTUEPageEnter");
    clearAudioTimeout();
    if (jamClient?.FTUEPageEnter) {
      await jamClient.FTUEPageEnter();
    }
  }, [jamClient, logger, clearAudioTimeout]);

  const FTUEPageLeave = useCallback(async () => {
    logger.debug("sessionUtils: FTUEPageLeave");
    clearAudioTimeout();
    if (jamClient?.FTUEPageLeave) {
      await jamClient.FTUEPageLeave();
    }
  }, [jamClient, logger, clearAudioTimeout]);

  const SessionPageEnter = useCallback(async () => {
    logger.debug("sessionUtils: SessionPageEnter");
    clearAudioTimeout();
    if (jamClient?.SessionPageEnter) {
      return await jamClient.SessionPageEnter();
    }
  }, [jamClient, logger, clearAudioTimeout]);

  const SessionPageLeave = useCallback(async () => {
    logger.debug("sessionUtils: SessionPageLeave");
    clearAudioTimeout();
    if (jamClient?.SessionPageLeave) {
      await jamClient.SessionPageLeave();
    }
  }, [jamClient, logger, clearAudioTimeout]);

  // Auto-open jam track functionality (from useSessionUtils)
  const autoOpenJamTrackRef = useRef(null);

  const setAutoOpenJamTrack = useCallback((jamTrack) => {
    logger.debug("setting auto-load jamtrack", jamTrack);
    autoOpenJamTrackRef.current = jamTrack;
  }, [logger]);

  const grabAutoOpenJamTrack = useCallback(() => {
    const jamTrack = autoOpenJamTrackRef.current;
    autoOpenJamTrackRef.current = null;
    logger.debug("grabbing auto-load jamtrack", jamTrack);
    return jamTrack;
  }, [logger]);

  // Latency data structure conversion (from useSessionUtils)
  const changeLatencyDataStructure = useCallback((data) => {
    return {
      id: data.user_id,
      audio_latency: data.audio_latency,
      full_score: data.ars['total_latency'],
      internet_score: data.ars['internet_latency']
    };
  }, []);

  // Score info calculation (from useSessionUtils)
  const scoreInfo = useCallback((userSession, isSameUser) => {
    const full_score = userSession.full_score;
    const internet_score = parseInt(userSession.internet_score);
    const audio_latency = parseInt(userSession.audio_latency);

    let latencyDescription;
    let latencyStyle;
    let iconName;
    let description;
    let latencyInfo;

    if (isSameUser) {
      latencyDescription = LATENCY.ME.description;
      latencyStyle = LATENCY.ME.style;
      iconName = 'purple';
      description = 'me';
      latencyInfo = '';
    } else if (full_score <= LATENCY.FAILED.max) {
      latencyDescription = LATENCY.FAILED.description;
      latencyStyle = LATENCY.FAILED.style;
      iconName = 'gray';
      description = 'failed';
      latencyInfo = '';
    } else if (!full_score || full_score <= LATENCY.UNKNOWN.max) {
      latencyDescription = LATENCY.UNKNOWN.description;
      latencyStyle = LATENCY.UNKNOWN.style;
      iconName = 'purple';
      description = 'missing';
      latencyInfo = '';
    } else if (full_score <= LATENCY.GOOD.max) {
      latencyDescription = LATENCY.GOOD.description;
      latencyStyle = LATENCY.GOOD.style;
      iconName = 'green';
      description = 'good';
      latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
    } else if (full_score <= LATENCY.MEDIUM.max) {
      latencyDescription = LATENCY.MEDIUM.description;
      latencyStyle = LATENCY.MEDIUM.style;
      iconName = 'yellow';
      description = 'fair';
      latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
    } else if (full_score <= LATENCY.POOR.max) {
      latencyDescription = LATENCY.POOR.description;
      latencyStyle = LATENCY.POOR.style;
      iconName = 'red';
      description = 'poor';
      latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
    } else if (full_score > LATENCY.UNACCEPTABLE.min) {
      latencyStyle = LATENCY.UNACCEPTABLE.style;
      latencyDescription = LATENCY.UNACCEPTABLE.description;
      iconName = 'blue';
      description = 'unacceptable';
      latencyInfo = 'Internet ' + internet_score + 'ms + Audio ' + audio_latency + 'ms';
    }

    return {
      latency_style: latencyStyle,
      latency_text: latencyDescription,
      icon_name: iconName,
      description: description,
      latency_info: latencyInfo
    };
  }, []);

  // Create latency info (from useSessionUtils)
  const createLatency = useCallback((userLatency) => {
    // Note: In React, we don't have access to currentUserId in the same way
    // This would need to be passed as a parameter or from context
    const isSameUser = userLatency.id === window.JK?.currentUserId; // Fallback for now
    return scoreInfo(userLatency, isSameUser);
  }, [scoreInfo]);

  // Join session from custom URL scheme (from useSessionUtils)
  const joinSessionFromCustomUrlScheme = useCallback((hash) => {
    const qStr = hash.substring(hash.lastIndexOf('/') + 1);
    const qParamsArr = qStr.split('|');
    let isCustom = undefined;
    let sessionId = undefined;

    qParamsArr.forEach((q) => {
      const qp = q.split('~');
      if (qp[0] === 'custom') {
        isCustom = qp[1];
      }
      if (qp[0] === 'sessionId') {
        sessionId = qp[1];
      }
    });

    if (!isCustom || isCustom !== 'yes') {
      return;
    }
    if (!sessionId) {
      return;
    }

    // Note: joinSession implementation would need to be provided
    // For now, just log
    logger.debug("Would join session from custom URL:", sessionId);
  }, [logger]);

  // Ensure session ended
  const ensureEnded = useCallback(() => {
    updateCurrentSession(null);
  }, [updateCurrentSession]);

  // Return public interface
  return {
    // Core session functions
    id,
    start,
    backingTrack,
    backingTracks,
    recordedBackingTracks,
    recordedJamTracks,
    setUserTracks: setUserTracksState,
    recordedTracks,
    jamTrackName,
    recordedJamTrackName,
    jamTracks,
    participants,
    joinSession,
    leaveSessionRest,
    leaveCurrentSession,
    refreshCurrentSession,
    updateSessionInfo,
    updateCurrentSession,
    subscribe,
    participantForClientId,
    isPlayingRecording,
    onWebsocketDisconnected,
    inSession,
    setMixerMode: setMixerModeState,
    isMasterMixMode,
    isPersonalMixMode,
    getMixMode,
    selfOpenedJamTracks,
    isMetronomeOpen,
    areControlsLockedForJamTrackRecording,
    lockControlsforJamTrackRecording,
    unlockControlsforJamTrackRecording,

    // Alert handlers
    onBackendMixerChanged,
    onDeadUserRemove,
    onWindowBackgrounded,
    waitForSessionPageEnterDone,
    onBroadcastStopped,
    onBroadcastSuccess,
    onBroadcastFailure,
    onPlaybackStateChange,

    // Event handlers from useSessionEnter
    onWatchedInputs,
    onMixersChanged,

    // Session leave functions from useSessionLeave
    setRecordingModel,
    leaveSession,
    handleLeaveSession,
    isLeaving,

    // Getters
    getCurrentSession: () => currentSession,
    getCurrentOrLastSession: () => currentOrLastSession,
    getParticipant: (clientId) => participantsEverSeen[clientId],
    setBackingTrack: setOpenBackingTrack,
    getBackingTrack: () => openBackingTrack,
    hasShownAudioMediaMixerHelp: () => shownAudioMediaMixerHelp,
    markShownAudioMediaMixerHelp: () => setShownAudioMediaMixerHelp(true),
    getMusicianAccess,

    // Audio establishment
    setAudioEstablished,

    // Cleanup
    ensureEnded,

    // Constants
    ALERT_TYPES,
    EVENTS,
    MIX_MODES,

    // Functions from useSessionUtils
    clearAudioTimeout,
    FTUEPageEnter,
    FTUEPageLeave,
    SessionPageEnter,
    SessionPageLeave,
    setAutoOpenJamTrack,
    grabAutoOpenJamTrack,
    changeLatencyDataStructure,
    scoreInfo,
    createLatency,
    joinSessionFromCustomUrlScheme,
    LATENCY
  };
}
