import { useCallback, useContext, useRef } from 'react';
import { useJamClient } from '../context/JamClientContext';
import { createDiagnostic } from '../helpers/rest';
import {
  ALERT_NAMES,
  ASSIGNMENT,
  VOICE_CHAT,
  AUDIO_DEVICE_BEHAVIOR,
  EVENTS,
  SYSTEM_DEFAULT_PLAYBACK_ONLY,
  JAMKAZAM_VIRTUAL_INPUT,
  SKIPPED_NETWORK_TEST
} from '../helpers/utils';

const useGearUtils = () => {
  const jamClient = useJamClient();
  const reloadAudioTimeout = useRef(null);
  const currentAudioRestartLocation = useRef(null);
  const skippedNetworkTest = useRef(false);

  const isChannelAssigned = useCallback((channel) => {
    return channel.assignment === ASSIGNMENT.CHAT || channel.assignment === ASSIGNMENT.OUTPUT || channel.assignment > 0;
  }, []);

  const resyncAudio = useCallback(async () => {
    const deferred = { resolve: null, reject: null };

    const backendMixerChange = (e, data) => {
      if (data.text === 'deferred') {
        jamClient.offBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange);
        deferred.resolve();
      }
    };

    const promise = new Promise((resolve, reject) => {
      deferred.resolve = resolve;
      deferred.reject = reject;
    });

    jamClient.onBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange);

    await jamClient.SessionAudioResync();

    // Give backend 5 seconds to timeout
    setTimeout(() => {
      deferred.reject(new Error('timeout'));
      jamClient.offBackendEvent(ALERT_NAMES.BACKEND_MIXER_CHANGE, 'gearUtilsResyncAudio', backendMixerChange);
    }, 5000);

    return promise;
  }, [jamClient]);

  const canPlayWithOthers = useCallback(async (profile) => {
    const isNoInputProfile = await isNoInputProfileFunc(profile);
    const expectedLatency = await jamClient.FTUEGetExpectedLatency();
    const audioLatency = expectedLatency ? expectedLatency.latency : null;

    let highLatency = false;
    if (audioLatency) {
      highLatency = audioLatency > 20 && audioLatency < 1000;
    }

    const networkScore = await jamClient.GetNetworkTestScore();
    const badNetworkScore = networkScore < 2;

    // For now, ignore latency
    if (window.gon?.global?.gear_check_ignore_high_latency) {
      highLatency = false;
    }

    return {
      canPlay: !isNoInputProfile && !highLatency,
      isNoInputProfile,
      badNetworkScore,
      highLatency,
      audioLatency,
      networkScore,
    };
  }, [jamClient]);

  const isNoInputProfileFunc = useCallback(async (profile) => {
    if (profile === undefined) {
      profile = await jamClient.LastUsedProfileName();
    }
    return profile === SYSTEM_DEFAULT_PLAYBACK_ONLY;
  }, [jamClient]);

  const canBeConfigured = useCallback((profile) => {
    return profile !== SYSTEM_DEFAULT_PLAYBACK_ONLY;
  }, []);

  const createProfileName = useCallback((deviceInfo, chatName) => {
    const isSameInOut = deviceInfo.input.id === deviceInfo.output.id;
    let name;
    if (isSameInOut) {
      name = `In/Out: ${deviceInfo.input.info.displayName}`;
    } else {
      name = `In: ${deviceInfo.input.info.displayName}, Out: ${deviceInfo.output.info.displayName}`;
    }
    console.log('creating profile name:', name);
    return name;
  }, []);

  const selectedDeviceInfo = useCallback(async (audioInputDeviceId, audioOutputDeviceId, deviceInformation) => {
    if (!audioInputDeviceId) {
      console.log('gearUtils.selectedDeviceInfo: no active input device');
      return null;
    }
    if (!audioOutputDeviceId) {
      console.log('gearUtils.selectedDeviceInfo: no active output device');
      return null;
    }

    if (!deviceInformation) {
      deviceInformation = await loadDeviceInfo();
    }

    const input = deviceInformation[audioInputDeviceId];
    const output = deviceInformation[audioOutputDeviceId];

    const inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type];
    const outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type];

    return {
      input: {
        id: audioInputDeviceId,
        info: input,
        behavior: inputBehavior,
      },
      output: {
        id: audioOutputDeviceId,
        info: output,
        behavior: outputBehavior,
      },
    };
  }, [jamClient]);

  const loadDeviceInfo = useCallback(async () => {
    const operatingSystem = await jamClient.GetOSAsString();
    // Should return one of: MacOSX_builtin, MACOSX_interface, Win32_wdm, Win32_asio, Win32_asio4all, Linux

    const determineDeviceType = async (deviceId, displayName) => {
      if (operatingSystem === 'MacOSX') {
        return displayName.toLowerCase().trim().startsWith('built-in') ? 'MacOSX_builtin' : 'MacOSX_interface';
      } else if (operatingSystem === 'Win32') {
        if (await jamClient.FTUEIsMusicDeviceWDM(deviceId)) {
          return 'Win32_wdm';
        } else if (displayName.toLowerCase().includes('asio4all')) {
          return 'Win32_asio4all';
        } else {
          return 'Win32_asio';
        }
      } else {
        return 'Linux';
      }
    };

    const devices = await jamClient.FTUEGetAudioDevices();
    console.log('DEVICE S DEVICES', devices.devices, devices.devices.length);

    const loadedDevices = {};

    for (const device of devices.devices) {
      if (device.name === 'JamKazam Virtual Monitor' || device.name === JAMKAZAM_VIRTUAL_INPUT) {
        continue;
      }

      if ((device.name === 'Default Input' || device.name === 'Default Output') && operatingSystem === 'Win32') {
        console.log('dropping', device.name);
        continue;
      }

      const deviceType = await determineDeviceType(device.guid, device.display_name);
      const deviceInfo = {
        id: device.guid,
        port_audio_name: device.port_audio_name,
        type: deviceType,
        displayType: AUDIO_DEVICE_BEHAVIOR[deviceType].display,
        displayName: device.display_name,
        inputCount: device.input_count,
        outputCount: device.output_count,
      };

      loadedDevices[device.guid] = deviceInfo;
    }

    console.log(Object.keys(loadedDevices).length + ' devices loaded.', loadedDevices);
    return loadedDevices;
  }, [jamClient]);

  const updateDefaultBuffers = useCallback(async (selectedDeviceInfo, frameBuffers) => {
    const hasWDMAssociated = selectedDeviceInfo && (selectedDeviceInfo.input.info.type === 'Win32_wdm' || selectedDeviceInfo.output.info.type === 'Win32_wdm');
    const hasASIOAssociated = selectedDeviceInfo && (selectedDeviceInfo.input.info.type === 'Win32_asio' || selectedDeviceInfo.output.info.type === 'Win32_asio');

    if (hasWDMAssociated || hasASIOAssociated) {
      const framesize = frameBuffers.selectedFramesize();

      if (framesize === 2.5) {
        if (hasWDMAssociated) {
          console.log('setting default buffers to 1/1');
          frameBuffers.setBufferIn('1');
          frameBuffers.setBufferOut('1');
        } else {
          console.log('setting default buffers to 0/0');
          frameBuffers.setBufferIn('0');
          frameBuffers.setBufferOut('0');
        }
      } else if (framesize === 5) {
        console.log('setting default buffers to 3/2');
        frameBuffers.setBufferIn('3');
        frameBuffers.setBufferOut('2');
      } else {
        console.log('setting default buffers to 2/2');
        frameBuffers.setBufferIn('2');
        frameBuffers.setBufferOut('2');
      }
    } else {
      console.log('setting default buffers to 0/0');
      frameBuffers.setBufferIn(0);
      frameBuffers.setBufferOut(0);
    }

    await jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
    await jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
  }, [jamClient]);

  const ftueSummary = useCallback(async (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) => {
    return {
      os: operatingSystem,
      modernClient: true,
      version: await jamClient.ClientUpdateVersion(),
      success: gearTest.isGoodFtue(),
      automated: isAutomated,
      score: {
        validLatencyScore: gearTest.isValidLatencyScore(),
        validIOScore: gearTest.isValidIOScore(),
        latencyScore: gearTest.getLatencyScore(),
        ioScore: gearTest.getIOScore(),
      },
      audioParameters: {
        frameSize: frameBuffers.selectedFramesize(),
        bufferIn: frameBuffers.selectedBufferIn(),
        bufferOut: frameBuffers.selectedBufferOut(),
      },
      devices: deviceInformation,
      selectedDevice: selectedDeviceInfo,
    };
  }, [jamClient]);

  const getProfiles = useCallback(async () => {
    const all = await jamClient.FTUEGetAllAudioConfigurations();
    const good = await jamClient.FTUEGetGoodAudioConfigurations();
    const current = await jamClient.LastUsedProfileName();

    const profiles = all.map((item) => ({
      id: item,
      good: false,
      class: 'bad',
      current: current === item,
    }));

    if (good) {
      good.forEach((goodItem) => {
        const profile = profiles.find((p) => p.id === goodItem);
        if (profile) {
          profile.good = true;
          profile.class = 'good';
        }
      });
    }

    return profiles;
  }, [jamClient]);

  const postDiagnostic = useCallback(async (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) => {
    createDiagnostic({
      type: 'GEAR_SELECTION',
      data: {
        client_type: 'client', // TODO: make dynamic
        client_id: jamClient.clientID,
        summary: await ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated),
      },
    });
  }, [jamClient, ftueSummary]);

  const getChatInputs = useCallback(async () => {
    const musicPorts = await jamClient.FTUEGetChannels();
    const chatsOnOtherDevices = await jamClient.FTUEGetChatInputs(false, true);

    // Remove virtual/remote junk
    Object.keys(chatsOnOtherDevices).forEach((key) => {
      if (key.includes('JamKazam')) {
        delete chatsOnOtherDevices[key];
      }
    });

    // WASAPI logic
    let allWasapi = true;
    Object.values(chatsOnOtherDevices).forEach((chatChannelName) => {
      if (!chatChannelName.includes('WASAPI')) {
        allWasapi = false;
      }
    });
    if (!allWasapi) {
      Object.keys(chatsOnOtherDevices).forEach((key) => {
        if (chatsOnOtherDevices[key].includes('WASAPI')) {
          delete chatsOnOtherDevices[key];
        }
      });
    }

    const chatInputs = [];

    musicPorts.inputs.forEach((input) => {
      if (input.assignment <= 0) {
        chatInputs.push({
          id: input.id,
          name: input.name,
          assignment: input.assignment,
        });
      }
    });

    for (const [chatChannelId, chatChannelName] of Object.entries(chatsOnOtherDevices)) {
      const chatInput = { id: chatChannelId, name: chatChannelName, assignment: null };
      const assignment = await jamClient.TrackGetAssignment(chatChannelId, true);
      chatInput.assignment = assignment;
      chatInputs.push(chatInput);
    }

    return chatInputs;
  }, [jamClient]);

  const isChannelAvailableForChat = useCallback((chatChannelId, musicPorts) => {
    return musicPorts.inputs.every((inputChannel) => {
      if (inputChannel.id === chatChannelId && inputChannel.assignment > 0) {
        return false;
      }
      return true;
    });
  }, []);

  const guardAgainstBadNetworkScore = useCallback((app) => {
    return new Promise(async (resolve, reject) => {
      resolve();
      if (!(await validNetworkScore())) {
        reject();
      } else {
        resolve();
      }
    });
  }, [jamClient]);

  const hasGoodActiveProfile = useCallback(async (verifyTracks) => {
    const hasOneConfigureDevice = await jamClient.hasOneConfiguredDevice();
    console.log('hasGoodActiveProfile:', hasOneConfigureDevice ? 'devices=has at least one configured device' : 'devices=has no configured device');
    return hasOneConfigureDevice;
  }, [jamClient]);

  const jamClientFTUEGetAllAudioConfigurations = useCallback(async () => {
    return await jamClient.FTUEGetAllAudioConfigurations();
  }, [jamClient]);

  const guardAgainstInvalidGearConfiguration = useCallback((app) => {
    return new Promise(async (resolve, reject) => {
      if ((await jamClientFTUEGetAllAudioConfigurations()).length === 0) {
        reject();
      } else {
        resolve();
      }
    });
  }, [jamClient, hasGoodActiveProfile]);

  const guardAgainstActiveProfileMissing = useCallback((app, backendInfo) => {
    return new Promise(async (resolve, reject) => {
      console.log('guardAgainstActiveProfileMissing: backendInfo %o', backendInfo);
      if (backendInfo.error && backendInfo.reason === 'no_profile' && (await jamClient.FTUEGetAllAudioConfigurations()).length > 0) {
        reject({ reason: 'handled', nav: '/client#/account/audio' });
        console.log('No Active Profile', 'We\'ve sent you to the audio profile screen...');
      } else if (backendInfo.error && backendInfo.reason === 'device_failure') {
        reject({ reason: 'handled', nav: '/client#/account/audio' });
      } else {
        resolve();
      }
    });
  }, [jamClient]);

  const guardAgainstInvalidConfiguration = useCallback((app, verifyNetworkScore) => {
    return new Promise(async (resolve, reject) => {
      try {
        await guardAgainstInvalidGearConfiguration(app);
        if (verifyNetworkScore) {
          await guardAgainstBadNetworkScore(app);
        }
        resolve();
      } catch (error) {
        reject();
      }
    });
  }, [guardAgainstInvalidGearConfiguration, guardAgainstBadNetworkScore]);

  const skipNetworkTest = useCallback(async () => {
    await jamClient.SetNetworkTestScore(SKIPPED_NETWORK_TEST);
    skippedNetworkTest.current = true;
  }, [jamClient]);

  const isNetworkTestSkipped = useCallback(() => {
    return skippedNetworkTest.current;
  }, []);

  const validNetworkScore = useCallback(async () => {
    return isNetworkTestSkipped() || (await jamClient.GetNetworkTestScore()) >= 2;
  }, [jamClient, isNetworkTestSkipped]);

  const isRestartingAudio = useCallback(() => {
    // Assuming some state
    return false;
  }, []);

  const scheduleAudioRestart = useCallback((location, initial_delay, beforeScan, afterScan, cancelScan) => {
    console.log('scheduleAudioRestart: (from ' + location + ')');

    let cancellable = true;

    const clearAudioReloadTimer = () => {
      if (!cancellable) return;
      if (cancelScan) cancelScan();
      else if (afterScan) afterScan(true);
      cancellable = false;
    };

    if (reloadAudioTimeout.current) {
      console.log('scheduleAudioRestart: clearing timeout (from ' + location + ')');
      clearTimeout(reloadAudioTimeout.current);
    }

    currentAudioRestartLocation.current = location;

    reloadAudioTimeout.current = setTimeout(() => {
      console.log('scheduleAudioRestart: rescan beginning (from ' + location + ')');
      cancellable = false;
      if (afterScan) afterScan(false);
    }, initial_delay || 5000);

    return clearAudioReloadTimer;
  }, []);

  const bootstrapDefaultPlaybackProfile = useCallback(async () => {
    const profiles = await getProfiles();
    let foundSystemDefaultPlaybackOnly = false;
    profiles.forEach((profile) => {
      if (profile.id === SYSTEM_DEFAULT_PLAYBACK_ONLY) {
        foundSystemDefaultPlaybackOnly = true;
      }
    });

    if (!foundSystemDefaultPlaybackOnly) {
      console.log('creating system default profile (playback only)');
      if (!(await createDefaultPlaybackOnlyProfile())) {
        console.error('unable to create the default playback profile!');
      }
    }
  }, [getProfiles]);

  const createDefaultPlaybackOnlyProfile = useCallback(async () => {
    const eMixerInputSampleRate = {
      JAMKAZAM_AUTO_SR: 0,
      USE_DEVICE_DEFAULT_SR: 1,
      PREFER_44: 2,
      PREFER_48: 3,
      PREFER_96: 4,
    };

    return await jamClient.FTUECreateUpdatePlayBackProfile(
      SYSTEM_DEFAULT_PLAYBACK_ONLY,
      eMixerInputSampleRate.USE_DEVICE_DEFAULT_SR,
      0, // buffering
      false // start audio
    );
  }, [jamClient]);

  // Guard against single player profile - simplified, as it depends on app
  //TODO: revisit this
  const guardAgainstSinglePlayerProfile = useCallback((app, beforeCallback) => {
    return new Promise(async (resolve, reject) => {
      const canPlayResult = await canPlayWithOthers();

      if (!canPlayResult.canPlay) {
        // TODO: Show dialog like legacy app with options:
        // - Create private session (controlled_location: true, redirect to new session)
        // - Go to audio settings (controlled_location: true, redirect to /account/audio)
        // - Cancel (controlled_location: false, redirect to home)

        // For now, mimic legacy behavior: reject and let caller redirect to home
        const error = new Error('Cannot play with others - single player profile detected');
        error.canPlayResult = canPlayResult;
        error.controlled_location = false; // Let caller handle redirect
        error.reason = 'single_player_profile';
        reject(error);
      } else {
        resolve();
      }
    });
  }, [canPlayWithOthers]);

  return {
    isChannelAssigned,
    resyncAudio,
    canPlayWithOthers,
    isNoInputProfile: isNoInputProfileFunc,
    canBeConfigured,
    createProfileName,
    selectedDeviceInfo,
    loadDeviceInfo,
    updateDefaultBuffers,
    ftueSummary,
    getProfiles,
    postDiagnostic,
    getChatInputs,
    isChannelAvailableForChat,
    guardAgainstBadNetworkScore,
    hasGoodActiveProfile,
    jamClientFTUEGetAllAudioConfigurations,
    guardAgainstInvalidGearConfiguration,
    guardAgainstActiveProfileMissing,
    guardAgainstInvalidConfiguration,
    skipNetworkTest,
    isNetworkTestSkipped,
    validNetworkScore,
    isRestartingAudio,
    scheduleAudioRestart,
    bootstrapDefaultPlaybackProfile,
    createDefaultPlaybackOnlyProfile,
    guardAgainstSinglePlayerProfile,
    SKIPPED_NETWORK_TEST,
    SYSTEM_DEFAULT_PLAYBACK_ONLY,
    JAMKAZAM_VIRTUAL_INPUT,
  };
};

export default useGearUtils;
