import { InstanceBase, InstanceStatus, Regex, runEntrypoint } from '@companion-module/base'
import { getActions } from './actions.js'
import { getPresets } from './presets.js'
import { getVariables } from './variables.js'
import { getFeedbacks } from './feedbacks.js'
import UpgradeScripts from './upgrades.js'

import OBSWebSocket, { EventSubscription } from 'obs-websocket-js'

import  util  from 'util'
import { exec } from 'child_process';
const exec1 = util.promisify(exec);

class CRE8Instance extends InstanceBase {
	constructor(internal) {
		super(internal)

		// set throttling for audio control settings
		this.lastSaveTime = 0
		this.saveThrottleMs = 1000
		this.saveTimeout = null
	}

	//Companion Internal and Configuration
	async init(config) {
		this.log('debug')
		this.config = config
		this.updateStatus(InstanceStatus.Connecting)

		if (this.config.host && this.config.port) {
			this.connectCRE8()
		} else if (this.config.host && !this.config.port) {
			this.updateStatus(InstanceStatus.BadConfig, 'Missing WebSocket Server port')
		} else if (!this.config.host && this.config.port) {
			this.updateStatus(InstanceStatus.BadConfig, 'Missing WebSocket Server IP address or hostname')
		} else {
			this.updateStatus(InstanceStatus.BadConfig, 'Missing WebSocket Server connection info')
		}
		

	}

	getConfigFields() {
		return [
			{
				type: 'textinput',
				id: 'host',
				label: 'Server IP / Hostname',
				width: 8,
			},
			{
				type: 'textinput',
				id: 'port',
				label: 'Server Port',
				width: 4,
				default: 4444,
				regex: Regex.PORT,
			},
			{
				type: 'textinput',
				id: 'pass',
				label: 'Server Password',
				width: 4,
			},
		]
	}

	async configUpdated(config) {
		this.config = config
		this.init(config)
	}

	async destroy() {
		// Save audio control settings before destroying
		try {
			await this.saveAudioControlSettings()
		} catch (error) {
			this.log('error', `Failed to save audio control settings during destroy: ${error.message}`)
		}
		
		// Clean up paused flash timer
		if (this.pausedFlashTimer) {
			clearInterval(this.pausedFlashTimer)
			this.pausedFlashTimer = null
		}
		
		this.disconnectCRE8()
		this.stopReconnectionPoll()
	}

	initVariables() {
		const variables = getVariables.bind(this)()
		this.setVariableDefinitions(variables)
	}

	initFeedbacks() {
		const feedbacks = getFeedbacks.bind(this)()
		this.setFeedbackDefinitions(feedbacks)
	}

	initPresets() {
		const presets = getPresets.bind(this)()
		this.setPresetDefinitions(presets)
	}

	initActions() {
		const actions = getActions.bind(this)()
		this.setActionDefinitions(actions)
	}

	//Utilities
	validName(name) {
		//Generate a valid name for use as a variable ID
		try {
			return name.replace(/[\W]/gi, '_')
		} catch (error) {
			this.log('debug', `Unable to generate validName for ${name}: ${error}`)
			return name
		}
	}

	formatTimecode(data) {
		//Converts milliseconds into a readable time format (hh:mm:ss)
		try {
			let formattedTime = new Date(data).toISOString().slice(11, 19)
			return formattedTime
		} catch (error) {}
	}

	roundNumber(number, decimalPlaces) {
		//Rounds a number to a specified number of decimal places
		try {
			return Number(Math.round(number + 'e' + decimalPlaces ?? 0) + 'e-' + decimalPlaces ?? 0)
		} catch (error) {
			this.log('debug', `Error rounding number ${number}: ${error}`)
			return number
		}
	}

	organizeChoices() {
		this.sourceChoices?.sort((a, b) => a.id.localeCompare(b.id))
		this.sceneChoices?.sort((a, b) => a.id.localeCompare(b.id))
		this.textSourceList?.sort((a, b) => a.id.localeCompare(b.id))
		// this.imageSourceList?.sort((a, b) => a.id.localeCompare(b.id))
		// this.mediaSourceList?.sort((a, b) => a.id.localeCompare(b.id))

		// const originalCount = this.mediaSourceList?.length || 0
		// this.mediaSourceList = this.mediaSourceList?.filter(source => {
		// 	return source.id.toLowerCase().includes('media player')
		// }) || []
		// 
		// if (originalCount !== this.mediaSourceList.length) {
		// 	this.log('debug', `Filtered mediaSourceList from ${originalCount} to ${this.mediaSourceList.length} sources for media tab control`)
		// }
		
		this.filterList?.sort((a, b) => a.id.localeCompare(b.id))
		this.audioSourceList?.sort((a, b) => a.id.localeCompare(b.id))
		
		const validMediaPlayerIds = this.mediaSourceList?.map(source => source.id) || []
		// this.log('debug', `Valid media player IDs for media tab sources: ${validMediaPlayerIds.join(', ')}`)
		const filteredTab = {}
		
		for (let i = 1; i <= 5; i++) {
			const currentSource = this.getVariableValue(`media_tab_${i}_source`)
			if (currentSource && currentSource !== '' && !validMediaPlayerIds.includes(currentSource)) {
				filteredTab[`media_tab_${i}_source`] = ''
				// this.log('debug', `cleared invalid media player source "${currentSource}" from media tab ${i}`)
			}
		}
		
		if (Object.keys(filteredTab).length > 0) {
			this.setVariableValues(filteredTab)
		}
		//Special Choices - Scenes
		this.sceneChoicesProgramPreview = [
			{ id: 'Current Scene', label: 'Current Scene' },
			{ id: 'Preview Scene', label: 'Preview Scene' },
		].concat(this.sceneChoices)
		this.sceneChoicesAnyScene = [{ id: 'anyScene', label: '<ANY SCENE>' }].concat(this.sceneChoices)
		// this.sceneChoicesCustomScene = [{ id: 'customSceneName', label: '<CUSTOM SCENE NAME>' }].concat(this.sceneChoices)
		this.sceneChoicesCustomScene = [
			// { id: 'customSceneName', label: '<CUSTOM SCENE NAME>' },
			{ id: '$(studio:scene_preview)', label: 'Current Preview Scene (Variable)' }
		].concat(this.sceneChoices)
		//Special Choices - Sources
		this.sourceChoicesWithScenes = this.sourceChoices.concat(this.sceneChoices)
		this.mediaSourceListCurrentMedia = [{ id: 'currentMedia', label: '<CURRENT MEDIA>' }].concat(this.mediaSourceList)
		//Default Choices
		this.sourceListDefault = this.sourceChoices?.[0] ? this.sourceChoices?.[0]?.id : ''
		this.sceneListDefault = this.sceneChoices?.[0] ? this.sceneChoices?.[0]?.id : ''
		this.filterListDefault = this.filterList?.[0] ? this.filterList?.[0]?.id : ''
		this.audioSourceListDefault = this.audioSourceList?.[0] ? this.audioSourceList?.[0]?.id : ''
		this.profileChoicesDefault = this.profileChoices?.[0] ? this.profileChoices[0].id : ''

		// vCam -> AUX and Scene defaults (read from variables so action dialogs can pre-populate)
		for (let i = 1; i <= 4; i++) {
			const auxVarName = `vcam_${i}_aux`
			const sceneVarName = `vcam_${i}_scene`
			this[`vcam${i}AuxDefault`] = this.getVariableValue(auxVarName) || (this.auxAudioList?.[0] ? this.auxAudioList[0].id : 'PGM')
			this[`vcam${i}SceneDefault`] = this.getVariableValue(sceneVarName) || (this.vcamSceneList?.[0] ? this.vcamSceneList[0].id : '')
		}

		// get vCam choices (or stored display names if set)
		this.vcamList = []
		for (let i = 1; i <= 4; i++) {
			const nameVar = this.getVariableValue(`vcam_${i}_name`)
			this.vcamList.push({ id: String(i), label: nameVar && nameVar !== '' ? nameVar : `vCam ${i}` })
		}

		// Build media tab choices dynamically based on detected VLC media sources
		this.mediaTabChoices = []
		
		if (this.mediaSourceList && this.mediaSourceList.length > 0) {
			for (let i = 0; i < this.mediaSourceList.length; i++) {
				const source = this.mediaSourceList[i]
				this.mediaTabChoices.push({
					id: String(i + 1),
					label: `${source?.label || 'unnamed'}`
				})
			}
			// this.log('debug', `Built ${this.mediaTabChoices.length} media tab choices from ${this.mediaSourceList.length} VLC media sources`)
		} else {
			this.mediaTabChoices.push({ id: '1', label: 'Media Tab 1 (no VLC sources detected)' })
			// this.log('debug', 'No VLC media sources detected, using default media tab choice')
		}
	}

	updateActionsFeedbacksVariables() {
		this.organizeChoices()

		this.initActions()
		this.initVariables()
		this.initFeedbacks()
		this.initPresets()
		this.checkFeedbacks()
		this.runTerminalCmd('IP4.ADDRESS','ip4');
		this.runTerminalCmd('IP4.GATEWAY','gateway');
		this.runTerminalCmd('IP4.DNS','dns');
	}

	// Audio Control Settings Persistence
	async saveAudioControlSettings() {
		if (this.saveTimeout) {
			clearTimeout(this.saveTimeout)
		}
		
		this.saveTimeout = setTimeout(async () => {
			try {
				const settings = {
					audio_control_source_1: this.getVariableValue('audio_control_source_1') || '',
					audio_control_source_2: this.getVariableValue('audio_control_source_2') || '',
					audio_control_source_3: this.getVariableValue('audio_control_source_3') || '',
					audio_control_source_4: this.getVariableValue('audio_control_source_4') || '',
					audio_control_type_1: this.getVariableValue('audio_control_type_1') ?? 0,
					audio_control_type_2: this.getVariableValue('audio_control_type_2') ?? 0,
					audio_control_type_3: this.getVariableValue('audio_control_type_3') ?? 0,
					audio_control_type_4: this.getVariableValue('audio_control_type_4') ?? 0,
					ip_1: this.getVariableValue('ip_1') || '',
					ip_2: this.getVariableValue('ip_2') || '',
					ip_3: this.getVariableValue('ip_3') || '',
					ip_4: this.getVariableValue('ip_4') || '',
				}
				
				this.log('debug', 'Saving audio control settings:', settings)
				
				// Only save if we have settings to save
				if (Object.values(settings).some(val => val !== '' && val !== 0)) {
					// Try to save to JSON file using dynamic import
					try {
						const fs = await import('fs')
						const path = await import('path')
						// Save to parent directory (sibling to StudioPro folder) to avoid Companion restrat/reconnection issues
						const settingsPath = path.join(process.cwd(), '..', 'audio_config', 'audio_control_settings.json')
						
						const settingsDir = path.dirname(settingsPath)
						if (!fs.existsSync(settingsDir)) {
							fs.mkdirSync(settingsDir, { recursive: true })
						}
						
						// write to .json file
						fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8')
						this.log('debug', 'Audio control settings saved to JSON file successfully')
					} catch (fileError) {
						this.log('warn', `Failed to save audio control settings to JSON file: ${fileError.message}`)

						// Fallback: try to save to vendor (if supported - will fail silently if not) - this doesn't work currently but doesn't error because the file save works ;-)
						this.sendRequest('CallVendorRequest', {
							vendorName: "cre8-app-main-controls",
							requestType: 'save_audio_control_settings',
							requestData: settings
						}).then(response => {
							this.log('debug', 'Audio control settings saved to vendor successfully:', response)
						}).catch(error => {
							this.log('debug', 'Audio control settings vendor save not supported yet:', error.message)
						})
					}
				} else {
					this.log('debug', 'No audio control settings to save (all empty/default)')
				}
			} catch (error) {
				this.log('error', `Error saving audio control settings: ${error.message}`)
			}
		}, 500) // Wait 500ms after last change before saving else it gets in a tizzy
	}

	async loadAudioControlSettings() {
		try {
			// Try to load from JSON file using dynamic import
			try {
				const fs = await import('fs')
				const path = await import('path')
				// Load from parent directory (sibling to StudioPro folder) to avoid Companion reconnection issues
				const settingsPath = path.join(process.cwd(), '..', 'audio_config', 'audio_control_settings.json')
				
				if (fs.existsSync(settingsPath)) {
					const fileContent = fs.readFileSync(settingsPath, 'utf8')
					const settings = JSON.parse(fileContent)
					this.log('debug', 'Audio control settings loaded from JSON file successfully')
					return settings
				}
			} catch (fileError) {
				this.log('warn', `Failed to load audio control settings from JSON file: ${fileError.message}`)
			}

			// Fallback: try to load from vendor - not supported yet(?) but will fail silently if not
			try {
				const response = await this.sendRequest('CallVendorRequest', {
					vendorName: "cre8-app-main-controls",
					requestType: 'load_audio_control_settings',
					requestData: { request: 'load' }
				})
				
				if (response?.responseData?.success && response.responseData.settings) {
					this.log('debug', 'Audio control settings loaded from vendor successfully')
					return response.responseData.settings
				}
			} catch (error) {
				this.log('debug', 'Audio control settings vendor load not supported yet')
			}
		} catch (error) {
			this.log('warn', `Failed to load audio control settings: ${error.message}`)
		}
		return null
	}

	async loadAudioControlSettingsAfterInit() {
		try {
			const settings = await this.loadAudioControlSettings()
			
			if (settings) {
				// Validate that the loaded sources still exist in the current audio source list
				const audioSourceList = this.audioSourceList.concat(this.auxAudioList)
				const validSources = audioSourceList.map(source => source.id)
				
				for (let i = 1; i <= 4; i++) {
					const sourceKey = `audio_control_source_${i}`
					const sourceValue = settings[sourceKey]
					
					if (sourceValue && !validSources.includes(sourceValue)) {
						this.log('warn', `Audio control source ${i} "${sourceValue}" no longer exists, clearing it`)
						settings[sourceKey] = ''
					}
				}
				
				// Update all vars with the loaded settings
				Object.entries(settings).forEach(([key, value]) => {
					this.setVariableValues({ [key]: value })
				})
				
				// Refresh audio source data for the loaded sources to make sure knobs are connected to current StudioPro state
				for (let i = 1; i <= 4; i++) {
					const sourceKey = `audio_control_source_${i}`
					const sourceValue = settings[sourceKey]
					
					if (sourceValue && validSources.includes(sourceValue)) {
						// Refresh audio data for this source
						try {
							await this.getSourceAudio(sourceValue)
							this.log('debug', `Refreshed audio data for loaded source: ${sourceValue}`)
						} catch (error) {
							this.log('warn', `Failed to refresh audio data for source ${sourceValue}: ${error.message}`)
						}
					}
				}
				
				// Update feedbacks to make sure state is correct
				this.checkFeedbacks('audio_control_type')
				
				this.log('debug', 'Audio control settings loaded and applied successfully after initialisation')
			} else {
				this.log('debug', 'No audio control settings to load, using defaults')
			}
		} catch (error) {
			this.log('warn', `Failed to load audio control settings after initialisation: ${error.message}`)
		}
	}

	// MARK: init state
	initializeStates() {
		//Basic Info
		this.scenes = []
		this.sources = {}
		this.states = {}
		this.transitions = {}
		this.profiles = {}
		this.sceneCollections = {}
		this.outputs = {}
		this.sceneItems = {}
		this.groups = {}

		// Anti-spam debounce for quick_transition / quick_cut
        this.states.quickActionBusy = false
		
		// initialise mix toggle state tracking
		this.states.mixPreviousScenes = {}
		// this.states.mixScenes = {}
		// this.states.mixToggleStates = {}
		//Source Types
		this.mediaSources = {}
		this.imageSources = {}
		this.textSources = {}
		this.sourceFilters = {}
		
		// Timer for paused flash effect only - doesn't interfere with play/pause feedback
		this.pausedFlashTimer = setInterval(() => {
			this.checkFeedbacks('media_tab_paused_flash', 'active_media_tab_paused_flash')
		}, 500)
		//Choices
		this.sceneChoices = []
		this.vcamSceneList = []
		this.sourceChoices = []
		this.profileChoices = []
		this.sceneCollectionList = []
		this.textSourceList = []
		this.mediaSourceList = []
		this.imageSourceList = []
		this.hotkeyNames = []
		this.imageFormats = []
		this.transitionList = []
		this.monitors = []
		this.outputList = []
		this.filterList = []
		this.audioSourceList = []
		this.mixList = []
		this.zoom_meetingList = []
		this.NDI_recordingList = []
		this.ISO_recordingList = []
		this.mediaTabChoices = []
		this.auxAudioList = [
			{id: "None", label: "None"},
			{id: "Program", label: "PGM"},
			{id: "Aux1", label: "Aux1"},
			{id: "Aux2", label: "Aux2"},
			{id: "Aux3", label: "Aux3"},
			{id: "Aux4", label: "Aux4"},
			{id: "Aux5", label: "Aux5"},
			{id: "Aux6", label: "Aux6"},
			{id: "Aux7", label: "Aux7"},
			{id: "Aux8", label: "Aux8"}
		] // || this.getAudioSources()
		// this.auxAudioList = []
		this.dskTabChoices = []
		this.dskItemChoices = []
		//Set Initial States
		this.vendorEvent = {}
		this.states.sceneCollectionChanging = false
		this.states.zoomMeetings = {}
		this.states.zoomMeetingValues = {}
		this.states.NDIRecordings = {}
		this.states.ISORecordings = {}

		const defaultAudioSettings = {
			audio_control_source_1: '',
			audio_control_source_2: '',
			audio_control_source_3: '',
			audio_control_source_4: '',
			audio_control_type_1: 0,
			audio_control_type_2: 0,
			audio_control_type_3: 0,
			audio_control_type_4: 0,
			ip_1: '',
			ip_2: '',
			ip_3: '',
			ip_4: ''
		}
		this.setVariableValues(defaultAudioSettings)

		// console.log('Initialising mix states')
		this.states.mix1Scene = 'None'
		this.states.mix2Scene = 'None'
		this.states.mix3Scene = 'None'
		this.states.mix4Scene = 'None'
		this.states.mix5Scene = 'None'
		this.states.mix6Scene = 'None'
		this.states.mix7Scene = 'None'
		this.states.mix8Scene = 'None'
		
		// Initialize storage for previous scenes before mix selection
		// this.states.mixPreviousScenes = {}
		
		// Initialise DSK states storage
		this.dskStates = {}
		// Initialise per-tab DSK item choices
		this.dskItemChoicesByTab = {}
		
		// Clear potentially stale DSK display and related variables on startup
		this.setVariableValues({
			dsk_display: '', // Clear any stale DSK display values
			active_dsk_tab: '1', // Reset to default tab
		});
		
		// Initialise DSK name variables
		const dskDefaults = {};
		for (let i = 0; i < 8; i++) {
			const tabNumber = i + 1;
			dskDefaults[`dsk_tab_${tabNumber}_name`] = '';
		}
		this.setVariableValues(dskDefaults);

		// Set initial button text based on current media state
		this.setVariableValues({
			current_media_button_text: 'Play', // Default to Play
		})

		// vCam choices (will be populated from variables in organizeChoices)
		this.vcamList = [
			{ id: '1', label: 'vCam 1' },
			{ id: '2', label: 'vCam 2' },
			{ id: '3', label: 'vCam 3' },
			{ id: '4', label: 'vCam 4' },
		]
	}

	resetSceneSourceStates() {
		this.scenes = []
		this.sources = {}
		this.mediaSources = {}
		this.imageSources = {}
		this.textSources = {}
		this.sourceFilters = {}
		this.groups = {}

		this.sceneChoices = []
		this.sourceChoices = []
		this.filterList = []
		this.audioSourceList = []
		this.mediaSourceList = []
		this.textSourceList = []
		this.imageSourceList = []
		this.mediaTabChoices = []
		this.dskTabChoices = []
		this.dskItemChoices = []
	}

	
	// async tbar () {
	// 	let i2c = null;
	// 	let ADS1115 = null;

	// 	try {
	// 		i2c = await import('./node_modules/i2c-bus/i2c-bus.js');
	// 		// console.log('i2c-bus installed', i2c.default.open);
	// 	} catch (error) {
	// 		console.log('i2c-bus is not installed. Skipping I2C device checks.');
	// 		return;
	// 	}
	
	// 	try {
	// 		ADS1115 = await import('./node_modules/ads1115/ads1115.js');
	// 		// console.log('ads1115 installed', ADS1115.default);
	// 	} catch (error) {
	// 		console.log('ADS1115 is not installed. Skipping ADS1115 checks.');
	// 		return;
	// 	}
		
	// 	i2c.default.openPromisified(1).then(async (bus) => {
	// 		// this.log('info', 'Connected to I2C bus', i2c)
	// 		const ads1115 = await ADS1115.default(bus)
	// 		// console.log('ads1115', ads1115)
	// 		ads1115.gain = 1
	// 		let oldvalue = 0.00;
	// 		let invert = false;

	// 		setInterval(async () => {
	// 			// console.log('before ads1115.measure', ads1115.measure)
	// 			let value = await ads1115.measure('0+GND')
	// 			let conversion = parseFloat(((value * 1)/18000).toFixed(2)); //18000 is the resistance of the potentiometer change to max value of value in log if incorrect resisters used
	// 			console.log('conversion', conversion)
	// 			console.log('value', value)

	// 			if(conversion > 1)
	// 			{
	// 				conversion = 1;
	// 			}
	// 			if(invert)
	// 			{
	// 				conversion = 1 - conversion;
	// 			}

	// 			if(conversion != oldvalue)
	// 			{

	// 				oldvalue = conversion;
	// 				await this.sendRequest('SetTBarPosition', {  release: true, position: conversion })
	// 			}

	// 			if(conversion >= 1)
	// 			{
	// 				invert = !invert;
	// 			}
				
	// 		}, 50);
			
	// 	})
	// }

	// MARK: CRE8 Connection
	//CRE8 Websocket Connection
	async connectCRE8() {
		if (this.cre8) {
			await this.cre8.disconnect()
		} else {
			this.cre8 = new OBSWebSocket()
		}
		try {
			const { cre8WebSocketVersion } = await this.cre8.connect(
				`ws://${this.config.host}:${this.config.port}`,
				this.config.pass,
				{
					eventSubscriptions:
						EventSubscription.All |
						EventSubscription.InputActiveStateChanged |
						EventSubscription.InputShowStateChanged |
						EventSubscription.InputVolumeMeters |
						EventSubscription.SceneItemTransformChanged,
					rpcVersion: 1,
				}
			)
			if (cre8WebSocketVersion) {
				this.updateStatus(InstanceStatus.Ok)
				this.stopReconnectionPoll()
				this.log('info', 'Connected to StudioPro')

				//Setup Initial State Objects
				this.initializeStates()

				//Get Initial CRE8 Info
				let initialInfo = await this.cre8Info()

				if (initialInfo) {
					//Start Listeners
					this.cre8Listeners()

					//Get Project Info
					this.getStats()
					this.getRecordStatus()
					//this.getStreamStatus()
					this.startStatsPoll()

					//Build General Parameters
					this.buildProfileList()
					this.buildSceneCollectionList()

					//Build Scene Collection Parameters
					this.buildSceneTransitionList()
					this.buildSpecialInputs()
					this.buildSceneList()
					this.buildDSKTabs()
					
					setTimeout(() => {
						this.loadAudioControlSettingsAfterInit()
					}, 1000) // load audio control settings after audio sources are built
				}
			}
			this.log('debug', 'Finished connectCRE8')
		} catch (error) {
			this.processWebsocketError(error)
		}
		// this.tbar();
	}

	processWebsocketError(error) {
		if (!this.reconnectionPoll) {
			let tryReconnect = null
			if (error?.message.match(/(Server sent no subprotocol)/i)) {
				tryReconnect = false
				this.log('error', 'Failed to connect to CRE8. Please upgrade CRE8 to version 28 or above')
				this.updateStatus(InstanceStatus.ConnectionFailure, 'Outdated websocket plugin')
			} else if (error?.message.match(/(missing an `authentication` string)/i)) {
				tryReconnect = false
				this.log(
					'error',
					`Failed to connect to CRE8. Please enter your WebSocket Server password in the module settings`
				)
			} else if (error?.message.match(/(Authentication failed)/i)) {
				tryReconnect = false
				this.log(
					'error',
					`Failed to connect to CRE8. Please ensure your WebSocket Server password is correct in the module settings`
				)
				this.updateStatus(InstanceStatus.BadConfig, 'Invalid password')
			} else if (error?.message.match(/(ECONNREFUSED)/i)) {
				tryReconnect = true
				this.log('error', `Failed to connect to CRE8. Please ensure CRE8 is open and reachable via your network`)
				this.updateStatus(InstanceStatus.ConnectionFailure)
			} else {
				tryReconnect = true
				this.log('error', `Failed to connect to CRE8 (${error.message})`)
				this.updateStatus(InstanceStatus.UnknownError)
			}
			if (tryReconnect) {
				this.startReconnectionPoll()
			}
		}
	}

	async disconnectCRE8() {
		if (this.cre8) {
			await this.cre8.disconnect()
			//Clear all active polls
			this.stopStatsPoll()
			this.stopMediaPoll()
		}
	}

	connectionLost() {
		if (!this.reconnectionPoll) {
			this.log('error', 'Connection lost to CRE8')
			this.updateStatus(InstanceStatus.Disconnected)
			this.disconnectCRE8()

			this.startReconnectionPoll()
		}
	}
	fetchCurrentSceneInfo() {
		this.log('debug', 'Fetching current scene information');
		
		// Get the current preview scene
		this.cre8.sendRequest('GetCurrentPreviewScene').then((response) => {
			this.log('debug', `Got current preview scene: ${JSON.stringify(response)}`);
			if (response && response.sceneName) {
				const currentPreview = response.sceneName;
				
				// Update our state if needed
				if (this.states.previewScene !== currentPreview) {
					this.log('debug', `Updating preview scene: "${this.states.previewScene}" -> "${currentPreview}"`);
					this.states.previewScene = currentPreview;
					this.setVariableValues({ scene_preview: currentPreview });
					
					// Force check all feedbacks
					this.checkFeedbacks();
					
					// Log the update
					this.log('debug', `Updated preview scene variable to "${currentPreview}" and checked feedbacks`);
				}
			}
		}).catch((error) => {
			this.log('error', `Failed to get current preview scene: ${error.message}`);
		});
		
		// Update scene choices
		this.updateSceneChoices();
	}	
	// MARK: Listeners
	// CRE8 Websocket Listeners
	async cre8Listeners() {
		//General
		this.cre8.once('ExitStarted', () => {
			this.connectionLost()
		})
		this.cre8.on('ConnectionClosed', () => {
			this.connectionLost()
		})
		this.cre8.on('VendorEvent', (data) => {
			console.log('VendorEvent received:', {
				vendorName: data.vendorName,
				eventType: data.eventType,
				eventData: data.eventData
			});
			
			this.vendorEvent = data
			this.checkFeedbacks('vendorEvent')

			// Sync active mix when the user switches tabs in the StudioPro GUI
			if (data && (data.eventType === 'MixTabChanged' || data.eventType === 'ActiveMixChanged' || data.eventType === 'MixChanged')) {
				const selected = data.eventData?.selected ?? data.eventData?.mixNumber ?? data.eventData?.mix
				if (selected) {
					console.log('[MIX-SYNC] Active mix changed from GUI:', selected)
					this.states.activeMix = selected
					this.checkFeedbacks('sceneMix')
				}
			}

			if (data && data.eventType === 'MixSceneChanged') {
				console.log('Mix scene event received:', data)
				const { mixNumber, sceneName } = data.eventData
				if (mixNumber >= 1 && mixNumber <= 8) {
					console.log(`Updating mix${mixNumber}_scene to: ${sceneName}`)
					this.states[`mix${mixNumber}Scene`] = sceneName
					this.setVariableValues({
						[`mix${mixNumber}_scene`]: sceneName
					})
					console.log('Updated mix states:', {
						states: this.states[`mix${mixNumber}Scene`],
						variable: this.getVariableValue(`mix${mixNumber}_scene`)
					})
					this.checkFeedbacks('sceneMix')
				}
			}

			if (data && data.vendorName === 'cre8-zoom-controls') {
				const eventData = data.eventData || {}
				const key = String(eventData.meetingNum)
				if (key && key !== 'undefined') {
					if (!this.states.zoomMeetings[key]) this.states.zoomMeetings[key] = {}
					if (data.eventType === 'joined_meeting' || data.eventType === 'hosted_meeting') {
						this.states.zoomMeetings[key].active = true
					} else if (data.eventType === 'left_meeting' || data.eventType === 'meeting_ended') {
						this.states.zoomMeetings[key].active = false
					}
					this.checkFeedbacks('zoom_meeting_active')
				}
			}

			if (data && data.vendorName === 'cre8-ndi-controls') {
				const eventData = data.eventData || {}
				const key = String(eventData.NDINum)
				if (key && key !== 'undefined') {
					const isActive = data.eventType === 'started'
					if (!this.states.NDIRecordings[key]) this.states.NDIRecordings[key] = {}
					this.states.NDIRecordings[key].active = isActive
					this.setVariableValues({ [`ndi_feed_${key}_active`]: isActive })
					this.checkFeedbacks('NDI_recording_active')
				}
			}

			if (data && data.vendorName === 'cre8-iso-controls') {
				const eventData = data.eventData || {}
				const key = String(eventData.ISONum)
				if (key && key !== 'undefined') {
					const isActive = data.eventType === 'started'
					if (!this.states.ISORecordings[key]) this.states.ISORecordings[key] = {}
					this.states.ISORecordings[key].active = isActive
					this.setVariableValues({ [`iso_feed_${key}_active`]: isActive })
					this.checkFeedbacks('ISO_recording_active')
				}
			}

			if (data && data.vendorName === 'downstream-keyer') {
				// console.log('Downstream-keyer vendor event received:', {
				// 	eventType: data.eventType,
				// 	eventData: data.eventData
				// });
				
				if (data.eventType === "dsk_updated") {
					console.log('DSK updated event received:', data);
					
					// Use the actual DSK data from the server response for updates
					if (data.eventData?.dskData) {
						const dskData = data.eventData.dskData;
						console.log('Processing DSK data from server:', dskData);
						
						// Update local state with server data instead of rebuilding everything
						if (typeof dskData === 'object') {
							// Handle both single item updates and multi updates
							for (const [tabKey, tabData] of Object.entries(dskData)) {
								// console.log('tabKey', tabKey, 'tabData', tabData);
								if (typeof tabData === 'object') {
									const tabIndex = parseInt(tabKey);
									if (!isNaN(tabIndex)) {
										// Initialise tab if it doesn't exist
										if (!this.dskStates[tabIndex]) {
											this.dskStates[tabIndex] = {};
										}
										
										// Update each DSK item state from server data (server data is the actual DSK data)
										for (const [itemName, itemState] of Object.entries(tabData)) {
											// console.log('itemName', itemName, 'itemState', itemState);
											const previousState = this.dskStates[tabIndex][itemName];
											this.dskStates[tabIndex][itemName] = itemState;
											console.log(`DSK state updated from server: Tab ${tabIndex}, ${itemName} = ${itemState} (was: ${previousState})`);
										}
									}
								}
							}
						}
						
						console.log('Current DSK states after update:', this.dskStates);
						
						// Update DSK display variable with current server state (server data is the actual DSK data)
						this.updateDSKDisplay(); // TODO: this is not working??!
						
						// Check DSK feedbacks after state update (server data is the actual DSK data)
						console.log('Checking DSK feedbacks after server state update...');
						this.checkFeedbacks('dsk_tab_active', 'dsk_display_active');
						
					} else {
						// if no dskData update provided from server, rebuild items for specific tab or all tabs (server data is the actual DSK data)
						if (data.eventData?.dskTabIdx !== undefined) {
							console.log(`Fallback coz no dskData provided from server: locally sorting DSK items for tab ${data.eventData.dskTabIdx}`);
							this.buildDSKItems(data.eventData.dskTabIdx);
						} else {
							console.log('Fallback coz no dskData provided from server: locally sorting all DSK tabs');
							this.buildDSKTabs(); // build all tabs
						}
						
						// Check DSK feedbacks after rebuild (server data is the actual DSK data)
						this.checkFeedbacks('dsk_tab_active', 'dsk_display_active');
					}
				}
			}
		})
		//Config
		this.cre8.on('CurrentSceneCollectionChanging', () => {
			this.stopMediaPoll()
			this.states.sceneCollectionChanging = true
		})
		this.cre8.on('CurrentSceneCollectionChanged', (data) => {
			this.states.currentSceneCollection = data.sceneCollectionName
			this.checkFeedbacks('scene_collection_active')
			this.setVariableValues({ scene_collection: this.states.currentSceneCollection })
			this.states.sceneCollectionChanging = false
			this.resetSceneSourceStates()
			this.buildSceneList()
			this.buildDSKTabs()
			this.buildSceneTransitionList()
			this.cre8Info()
		})
		this.cre8.on('SceneCollectionListChanged', () => {
			this.buildSceneCollectionList()
		})
		this.cre8.on('CurrentProfileChanging', () => {})
		this.cre8.on('CurrentProfileChanged', (data) => {
			this.states.currentProfile = data.profileName
			this.checkFeedbacks('profile_active')
			this.setVariableValues({ profile: this.states.currentProfile })
			this.cre8Info()
		})
		this.cre8.on('ProfileListChanged', () => {
			this.buildProfileList()
		})
		//Scenes
		this.cre8.on('SceneCreated', (data) => {
			if (data?.isGroup === false && this.states.sceneCollectionChanging === false) {
				this.addScene(data.sceneName)
			}
		})
		this.cre8.on('SceneRemoved', (data) => {
			if (data?.isGroup === false && this.states.sceneCollectionChanging === false) {
				this.removeScene(data.sceneName)
			}
		})
		this.cre8.on('SceneNameChanged', (data) => {
			this.log('debug', `Scene name changed event received: ${JSON.stringify(data)}`);
			if (this.sceneItems[data.oldSceneName]) {
				this.sceneItems[data.sceneName] = this.sceneItems[data.oldSceneName]
				delete this.sceneItems[data.oldSceneName]
			}
			let scene = this.sceneChoices.findIndex((item) => item.id === data.oldSceneName)
			this.sceneChoices.splice(scene, 1)
			this.sceneChoices.push({ id: data.sceneName, label: data.sceneName })

			this.updateActionsFeedbacksVariables()
		})
		this.cre8.on('CurrentProgramSceneChanged', (data) => {
			this.states.programScene = data.sceneName
			this.setVariableValues({ scene_active: this.states.programScene })
			this.checkFeedbacks('scene_active')
			this.checkFeedbacks('sceneProgram')
			this.checkFeedbacks('sceneMix')
			
			// sync toggle states when program scene changes
			// check if the new program scene matches any stored mix or previous scenes
			// if (this.states.mixToggleStates && this.states.mixScenes && this.states.mixPreviousScenes) {
			// 	Object.keys(this.states.mixToggleStates).forEach((mixNumber) => {
			// 		const mixScene = this.states.mixScenes[mixNumber];
			// 		const previousScene = this.states.mixPreviousScenes[mixNumber];
					
			// 		if (data.sceneName === mixScene) {
			// 			// program changed to the mix scene
			// 			this.states.mixToggleStates[mixNumber] = 'mix';
			// 		} else if (data.sceneName === previousScene) {
			// 			// program changed to the previous scene
			// 			this.states.mixToggleStates[mixNumber] = 'previous';
			// 		}
			// 	});
			// }
		})
		this.cre8.on('CurrentPreviewSceneChanged', (data) => {
			this.states.previewScene = data.sceneName ?? 'None'
			this.setVariableValues({ scene_preview: this.states.previewScene })
			this.checkFeedbacks('scene_active')
			this.checkFeedbacks('scenePreview')
		})

		this.cre8.on('SceneListChanged', (data) => {
			this.scenes = data.scenes
		})

		this.cre8.on('SceneNameChanged', (data) => {
			this.log('debug', `Scene name changed event received: ${JSON.stringify(data)}`);
			
			if (!data.oldSceneName) {
				this.log('warn', 'SceneNameChanged event missing oldSceneName');
				return;
			}
			const newSceneName = data.newSceneName || data.sceneName;
			
			if (!newSceneName) {
				this.log('warn', 'SceneNameChanged event missing new scene name, fetching current scene info');
				this.fetchCurrentSceneInfo();
				return;
			}
			for (let i = 0; i < 1000 ; i++) {
				if (this.getVariableValue(`scene_${i}`) === data.oldSceneName) {
					this.setVariableValues({ [`scene_${i}`]: newSceneName });
					break;
				}
			}
			if (this.states.previewScene === data.oldSceneName) {
				this.log('debug', `Updating preview scene name: "${data.oldSceneName}" -> "${newSceneName}"`);
				this.states.previewScene = newSceneName;
				// this.setVariableValues({ scene_preview: newSceneName });
				// this.setVariableValues({ scene_3: newSceneName });
				
				this.checkFeedbacks();
				
				this.log('debug', `Updated preview scene variable to "${newSceneName}" and checked feedbacks`);
			}
			
			this.updateSceneChoices();
		})
		
		this.cre8.on('SceneListChanged', (data) => {
			// this.log('debug', `Scene list changed event received: ${JSON.stringify(data)}`);
			
			this.updateSceneChoices();
			
			this.fetchCurrentSceneInfo();

			this.cre8.sendRequest('GetCurrentPreviewScene').then(response => {
				if (response && response.sceneName) {
					const currentPreview = response.sceneName;
					
					if (this.states.previewScene !== currentPreview) {
						this.log('debug', `Updating preview scene after list change: "${this.states.previewScene}" -> "${currentPreview}"`);
						this.states.previewScene = currentPreview;
						this.setVariableValues({ scene_preview: currentPreview });
						this.checkFeedbacks();
					}
				}
			}).catch(error => {
				this.log('error', `Failed to get current preview scene: ${error.message}`);
			});
		})


		//Inputs
		this.cre8.on('InputCreated', (data) => {})
		this.cre8.on('InputRemoved', (data) => {
			let source = this.sourceChoices.findIndex((item) => item.id == data.inputName)
			if (source > -1) {
				this.sourceChoices.splice(source, 1)
			}
			delete this.sources[data.inputName]
			this.updateActionsFeedbacksVariables()
		})
		this.cre8.on('InputNameChanged', () => {
			this.log('debug', `Input Name Changed: ${data}`)
			this.checkFeedbacks('scenePreview')
		})		
		this.cre8.on('InputActiveStateChanged', (data) => {
			if (this.sources[data.inputName]) {
				this.sources[data.inputName].active = data.videoActive
				this.checkFeedbacks('scene_item_active')
			} 
		})
		this.cre8.on('InputShowStateChanged', (data) => {
			if (this.sources[data.inputName]) {
				this.sources[data.inputName].videoShowing = data.videoShowing
				this.checkFeedbacks('scene_item_previewed')
			}
		})
		this.cre8.on('InputMuteStateChanged', (data) => {
			this.sources[data.inputName].inputMuted = data.inputMuted
			let name = this.sources[data.inputName].validName
			this.setVariableValues({
				[`mute_${name}`]: this.sources[data.inputName].inputMuted ? 'Muted' : 'Unmuted',
			})
			this.checkFeedbacks('audio_muted')
		})
		this.cre8.on('InputVolumeChanged', (data) => {
			this.log('debug', `Input Volume Changed: ${JSON.stringify(data)}`)
			this.sources[data.inputName].inputVolume = this.roundNumber(data.inputVolumeDb, 1)
			let name = this.sources[data.inputName].validName
			this.setVariableValues({ [`volume_${name}`]: this.sources[data.inputName].inputVolume + 'db' })
			this.checkFeedbacks('volume')
		})
		this.cre8.on('InputAudioBalanceChanged', (data) => {
			this.sources[data.inputName].inputAudioBalance = this.roundNumber(data.inputAudioBalance, 1)
			let name = this.sources[data.inputName].validName
			this.setVariableValues({ [`balance_${name}`]: this.sources[data.inputName].inputAudioBalance })
		})
		this.cre8.on('InputAudioSyncOffsetChanged', (data) => {
			this.sources[data.inputName].inputAudioSyncOffset = data.inputAudioSyncOffset
			let name = this.sources[data.inputName].validName
			this.setVariableValues({
				[`sync_offset_${name}`]: this.sources[data.inputName].inputAudioSyncOffset + 'ms',
			})
		})
		this.cre8.on('InputAudioTracksChanged', () => {})
		this.cre8.on('InputAudioMonitorTypeChanged', (data) => {
			this.sources[data.inputName].monitorType = data.monitorType
			let name = this.sources[data.inputName].validName
			let monitorType
			if (data.monitorType === 'CRE8_MONITORING_TYPE_MONITOR_AND_OUTPUT') {
				monitorType = 'Monitor / Output'
			} else if (data.monitorType === 'CRE8_MONITORING_TYPE_MONITOR_ONLY') {
				monitorType = 'Monitor Only'
			} else {
				monitorType = 'Off'
			}
			this.setVariableValues({ [`monitor_${name}`]: monitorType })
			this.checkFeedbacks('audio_monitor_type')
		})
		this.cre8.on('InputVolumeMeters', (data) => {
			this.updateAudioPeak(data)
		})
		this.cre8.on('InputSettingsChanged', (data) => {
			this.log('debug', `Input Settings Changed: ${data}`)
			let source = data.inputName
			let settings = data.inputSettings

			this.updateInputSettings(source, settings)
		})
		//Transitions
		this.cre8.on('CurrentSceneTransitionChanged', async (data) => {
			let transition = await this.sendRequest('GetCurrentSceneTransition')

			this.states.currentTransition = data.transitionName
			this.states.transitionDuration = transition?.transitionDuration ?? '0'

			this.checkFeedbacks('transition_duration', 'current_transition')
			this.setVariableValues({
				current_transition: this.states.currentTransition,
				transition_duration: this.states.transitionDuration,
			})
		})
		this.cre8.on('CurrentSceneTransitionDurationChanged', (data) => {
			this.states.transitionDuration = data.transitionDuration ?? '0'
			this.checkFeedbacks('transition_duration')
			this.setVariableValues({ transition_duration: this.states.transitionDuration })
		})
		this.cre8.on('SceneTransitionStarted', () => {
			this.states.transitionActive = true
			this.setVariableValues({ transition_active: 'True' })
			this.checkFeedbacks('transition_active')
		})
		this.cre8.on('SceneTransitionEnded', () => {
			this.states.transitionActive = false
			this.setVariableValues({ transition_active: 'False' })
			this.checkFeedbacks('transition_active')
		})
		this.cre8.on('SceneTransitionVideoEnded', () => {})
		//Filters
		this.cre8.on('SourceFilterListReindexed', () => {})
		this.cre8.on('SourceFilterCreated', (data) => {
			this.getSourceFilters(data.sourceName)
		})
		this.cre8.on('SourceFilterRemoved', (data) => {
			this.getSourceFilters(data.sourceName)
		})
		this.cre8.on('SourceFilterNameChanged', () => {})
		this.cre8.on('SourceFilterEnableStateChanged', (data) => {
			if (this.sourceFilters[data.sourceName]) {
				let filter = this.sourceFilters[data.sourceName].findIndex((item) => item.filterName == data.filterName)
				if (filter !== undefined) {
					this.sourceFilters[data.sourceName][filter].filterEnabled = data.filterEnabled
					this.checkFeedbacks('filter_enabled')
				}
			}
		})
		//Scene Items
		this.cre8.on('SceneItemCreated', (data) => {
			this.log('debug', `Scene Item Created: ${data.sceneName} - ${data.sceneItemId}`)
			if (this.states.sceneCollectionChanging === false) {
				this.buildSourceList(data.sceneName)
			}
		})
		this.cre8.on('SceneItemRemoved', (data) => {
			if (this.states.sceneCollectionChanging === false) {
				let item = this.sceneItems[data.sceneName].findIndex((item) => item.sceneItemId === data.sceneItemId)
				if (item > -1) {
					this.sceneItems[data.sceneName].splice(item, 1)
				}
			}
		})
		this.cre8.on('SceneItemListReindexed', () => {})
		this.cre8.on('SceneItemEnableStateChanged', (data) => {
			if (this.groups[data.sceneName]) {
				let sceneItem = this.groups[data.sceneName].findIndex((item) => item.sceneItemId === data.sceneItemId)
				this.groups[data.sceneName][sceneItem].sceneItemEnabled = data.sceneItemEnabled
			} else {
				let sceneItem = this.sceneItems[data.sceneName].findIndex((item) => item.sceneItemId === data.sceneItemId)
				this.sceneItems[data.sceneName][sceneItem].sceneItemEnabled = data.sceneItemEnabled
			}
			this.checkFeedbacks('scene_item_active_in_scene')
		})
		this.cre8.on('SceneItemLockStateChanged', () => {})
		this.cre8.on('SceneItemSelected', () => {})
		this.cre8.on('SceneItemTransformChanged', () => {})
		//Outputs
		this.cre8.on('StreamStateChanged', (data) => {
			this.states.streaming = data.outputActive

			this.setVariableValues({ streaming: this.states.streaming ? 'Live' : 'Off-Air' })
			this.checkFeedbacks('streaming', 'streamCongestion')
		})
		this.cre8.on('RecordStateChanged', (data) => {
			if (data.outputActive === true) {
				this.states.recording = 'Recording'
			} else {
				if (data.outputState === 'CRE8_WEBSOCKET_OUTPUT_PAUSED') {
					this.states.recording = 'Paused'
				} else {
					this.states.recording = 'Stopped'
					this.setVariableValues({ recording_timecode: '00:00:00' })
				}
			}
			if (data.outputPath) {
				this.setVariableValues({ recording_file_name: data.outputPath.match(/[^\\\/]+(?=\.[\w]+$)|[^\\\/]+$/) })
			}
			this.setVariableValues({ recording: this.states.recording })
			this.checkFeedbacks('recording')
		})
		this.cre8.on('ReplayBufferStateChanged', (data) => {
			this.states.replayBuffer = data.outputActive
			this.checkFeedbacks('replayBufferActive')
		})
		this.cre8.on('VirtualcamStateChanged', (data) => {
			this.outputs['virtualcam_output'].outputActive = data.outputActive
			this.checkFeedbacks('output_active')
		})

		this.cre8.on('VcamMappingChanged', (data) => {
			try {
				const v = data.vcamNumber
				const a = data.auxName
				this.log('info', `setVcamAux HIT: vCam ${v} -> ${a}`)
				if (v != null && a != null) {
					this.setVariableValues({ [`vcam_${v}_aux`]: a })
					this.checkFeedbacks()
				}
			} catch (e) {
				this.log('error', `Error handling VcamMappingChanged: ${e}`)
			}
		})

		this.cre8.on('VendorRequestReceived', (data) => {
			try {
				// Expecting shape: { vendorName, requestType, requestData }
				if (data?.vendorName === 'cre8-app-main-controls' && data?.requestType === 'setVcamAux') {
					const v = data.requestData?.vcamNumber
					const a = data.requestData?.auxName
					this.log('info', `setVcamAux HIT: vCam ${v} -> ${a}`)
					if (v != null && a != null) {
						this.setVariableValues({ [`vcam_${v}_aux`]: a })
						this.checkFeedbacks()
					}
				}
			} catch (e) {
				this.log('error', `Error handling VendorRequestReceived: ${e}`)
			}
		})
		this.cre8.on('ReplayBufferSaved', (data) => {
			this.setVariableValues({ replay_buffer_path: data.savedReplayPath })
		})
		//Media Inputs
		this.cre8.on('MediaInputPlaybackStarted', (data) => {
			this.states.currentMedia = data.inputName

			let name = this.sources[data.inputName].validName
			this.setVariableValues({
				current_media_name: this.states.currentMedia,
				current_media_button_text: 'Pause', // Show "Pause" when playing
				[`media_status_${name}`]: 'Playing',
			})
		})
		this.cre8.on('MediaInputPlaybackEnded', (data) => {
			if (this.states.currentMedia == data.inputName) {
				let name = this.sources[data.inputName].validName
				this.setVariableValues({
					current_media_name: 'None',
					current_media_button_text: 'Play', // Show "Play" when stopped
					[`media_status_${name}`]: 'Stopped',
				})
			}
		})
		this.cre8.on('MediaInputActionTriggered', (data) => {
			let name = this.sources[data.inputName]?.validName
			if (!name) return
			
			if (data.mediaAction == 'CRE8_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE') {
				this.setVariableValues({ 
					[`media_status_${name}`]: 'Paused',
					current_media_button_text: 'Play', // Show "Play" when paused
				})
			} else if (data.mediaAction == 'CRE8_WEBSOCKET_MEDIA_INPUT_ACTION_LOOP') {
				// Loop action triggered - refresh media status to get updated loop state
				setTimeout(() => {
					this.getMediaStatus()
				}, 100)
			}
		})
		//UI
		this.cre8.on('StudioModeStateChanged', async (data) => {
			this.log('debug', `Studio Mode state changed: ${data}`)
			this.states.studioMode = data.studioModeEnabled ? true : false
			this.checkFeedbacks('studioMode')

			if (this.states.studioMode) {
				let preview = await this.sendRequest('GetCurrentPreviewScene')
				this.states.previewScene = preview?.sceneName ?? 'None'
			} else {
				this.states.previewScene = 'None'
			}
			this.checkFeedbacks('studioMode', 'scenePreview')
			this.setVariableValues({ scene_preview: this.states.previewScene })
		})

		this.cre8.on('DSKChanged', (data) => { // TODO: Handle DSK changes - SE to implement socket event
			this.log('debug', `DSK Changed: ${JSON.stringify(data)}`);
		})

		this.cre8.on('MixSceneChanged', async (data) => {
			this.log('debug', `MixSceneChanged: ${JSON.stringify(data)}`)
			await this.refreshMixSceneStates()
		})
		this.cre8.on('ZoomMeetingStateChanged', async (data) => {
			this.log('debug', `ZoomMeetingStateChanged: ${JSON.stringify(data)}`)
			await this.refreshZoomMeetingStates()
		})
		//MARK: NDI&ISO listeners
		this.cre8.on('ISORecordingStateChanged', (data) => {
			this.log('debug', `ISORecordingStateChanged: ${JSON.stringify(data)}`)
			const num = String(data.recorderNumber)
			const isActive = data.status === 'active'
			if (!this.states.ISORecordings[num]) this.states.ISORecordings[num] = {}
			this.states.ISORecordings[num].active = isActive
			this.setVariableValues({ [`iso_feed_${num}_active`]: isActive })
			this.checkFeedbacks('ISO_recording_active')
		})
		this.cre8.on('NDIRecordingStateChanged', (data) => {
			this.log('debug', `NDIRecordingStateChanged: ${JSON.stringify(data)}`)
			const num = String(data.recorderNumber)
			const isActive = data.status === 'active'
			if (!this.states.NDIRecordings[num]) this.states.NDIRecordings[num] = {}
			this.states.NDIRecordings[num].active = isActive
			this.setVariableValues({ [`ndi_feed_${num}_active`]: isActive })
			this.checkFeedbacks('NDI_recording_active')
		})
		
	}

	// MARK: Websocket Cmnds
	//CRE8 Websocket Commands
	async sendRequest(requestType, requestData) {
		// this.log('debug', `Calling websocket method: ${requestType} with data: ${JSON.stringify(requestData)}`)
		try {
			let data = await this.cre8.call(requestType, requestData)
			// this.log('debug', `Websocket method ${requestType} response: ${JSON.stringify(data)}`)
			return data
		} catch (error) {
			this.log('debug', `Request ${requestType ?? ''} failed (${error})`)
		}
	}

	async sendBatch(batch) {
		try {
			let data = await this.cre8.callBatch(batch)

			let errors = data.filter((request) => request.requestStatus.result === false)
			if (errors.length > 0) {
				let errorMessages = errors.map((error) => error.requestStatus.comment).join(' // ')
				this.log('debug', `Partial batch request failure (${errorMessages})`)
			}

			return data
		} catch (error) {
			this.log('debug', `Batch request failed (${error})`)
		}
	}

	// MARK: Polling
	startReconnectionPoll() {
		this.stopReconnectionPoll()
		this.reconnectionPoll = setInterval(() => {
			this.connectCRE8()
		}, 5000)
	}

	stopReconnectionPoll() {
		if (this.reconnectionPoll) {
			clearInterval(this.reconnectionPoll)
			delete this.reconnectionPoll
		}
	}

	startStatsPoll() {
		this.stopStatsPoll()
		if (this.cre8) {
			this.statsPoll = setInterval(() => {
				this.getStats()
				if (this.states.streaming) {
					this.getStreamStatus()
				}
				if (this.states.recording === 'Recording') {
					this.getRecordStatus()
				}
				if (this.outputs) {
					for (let outputName in this.outputs) {
						this.getOutputStatus(outputName)
					}
				}
			}, 1000)
		}
	}

	stopStatsPoll() {
		if (this.statsPoll) {
			clearInterval(this.statsPoll)
			delete this.statsPoll
		}
	}

	startMediaPoll() {
		this.stopMediaPoll()
		this.mediaPoll = setInterval(() => {
			this.getMediaStatus()
		}, 1000)
	}

	stopMediaPoll() {
		if (this.mediaPoll) {
			clearInterval(this.mediaPoll)
			this.mediaPoll = null
		}
	}

	// MARK: builds & refresh
	async cre8Info() {
		try {
			let version = await this.sendRequest('GetVersion')
			this.states.version = version
			this.log(
				'debug',
				`CRE8 Version: ${version.cre8Version} // CRE8 Websocket Version: ${version.cre8WebSocketVersion} // Platform: ${version.platformDescription}`
			)
			version.supportedImageFormats.forEach((format) => {
				this.imageFormats.push({ id: format, label: format })
			})

			let studioMode = await this.sendRequest('GetStudioModeEnabled')
			this.states.studioMode = studioMode.studioModeEnabled ? true : false

			this.buildHotkeyList()
			this.buildOutputList()
			this.buildMonitorList()
			this.buildMixList()
			this.buildZoomMeetingList()
			this.buildNDIRecordingList()
			this.buildISORecordingList()
			this.getVideoSettings()
			this.getReplayBufferStatus()
			return true
		} catch (error) {
			this.log('debug', error)
			return false
		}
	}

	async buildHotkeyList() {
		let hotkeyList = await this.sendRequest('GetHotkeyList')
		hotkeyList?.hotkeys?.forEach((hotkey) => {
			this.hotkeyNames.push({ id: hotkey, label: hotkey })
		})
		this.updateActionsFeedbacksVariables()
	}

	async buildProfileList() {
		let profiles = await this.sendRequest('GetProfileList')
		this.profileChoices = []

		this.states.currentProfile = profiles?.currentProfileName

		profiles?.profiles.forEach((profile) => {
			this.profileChoices.push({ id: profile, label: profile })
		})

		this.checkFeedbacks('profile_active')
		this.setVariableValues({ profile: this.states.currentProfile })
		this.updateActionsFeedbacksVariables()
	}

	async buildSceneCollectionList() {
		let collections = await this.sendRequest('GetSceneCollectionList')
		this.sceneCollectionList = []

		this.states.currentSceneCollection = collections?.currentSceneCollectionName
		collections?.sceneCollections.forEach((sceneCollection) => {
			this.sceneCollectionList.push({ id: sceneCollection, label: sceneCollection })
		})

		this.checkFeedbacks('scene_collection_active')
		this.setVariableValues({ scene_collection: this.states.currentSceneCollection })

		this.updateActionsFeedbacksVariables()
	}

	async buildSpecialInputs() {
		let specialInputs = await this.sendRequest('GetSpecialInputs')
		if (specialInputs) {
			for (let x in specialInputs) {
				let input = specialInputs[x]

				if (input) {
					this.sources[input] = {
						sourceName: input,
						validName: this.validName(input),
					}

					if (!this.sourceChoices.find((item) => item.id === input)) {
						this.sourceChoices.push({ id: input, label: input })
					}
					this.getAudioSources(input)
				}
			}
		}
	}

	async buildOutputList() {
		this.outputs = {}
		this.outputList = []

		let outputData = await this.sendRequest('GetOutputList')

		if (outputData) {
			outputData.outputs?.forEach((output) => {
				let outputKind = output.outputKind
				if (outputKind === 'virtualcam_output') {
					this.outputList.push({ id: 'virtualcam_output', label: 'Virtual Camera' })
				} else if (
					outputKind != 'ffmpeg_muxer' &&
					outputKind != 'ffmpeg_output' &&
					outputKind != 'replay_buffer' &&
					outputKind != 'rtmp_output'
				) {
					//The above outputKinds are handled separately by other actions, so they are omitted
					this.outputList.push({ id: output.outputName, label: output.outputName })
				}
				this.getOutputStatus(output.outputName)
			})
			this.updateActionsFeedbacksVariables()
		}
	}

	async buildMonitorList() {
		let monitorList = await this.sendRequest('GetMonitorList')
		this.states.monitors = monitorList

		if (monitorList) {
			monitorList.monitors?.forEach((monitor) => {
				let monitorName = monitor.monitorName ?? `Display ${monitor.monitorIndex}`

				this.monitors.push({
					id: monitor.monitorIndex,
					label: `${monitorName} (${monitor.monitorWidth}x${monitor.monitorHeight})`,
				})
			})
		}
	}

	async buildMixList() {
		this.mixList = []

		const populatedMixNums = new Set()

		try {
			const resp = await this.sendRequest("GetMixSceneOptions", {})

			// console.log('[MIX-INIT] GetMixSceneOptions response:', resp)

			if (Array.isArray(resp)) {
				// Original format: plain array of mix objects [{ name, sceneName }, ...]
				// Index 0 = Mix 1, index 1 = Mix 2, etc. PROGRAM is not included.
				this.mixList.push({ id: 'PROGRAM', label: 'Program' })

				resp.forEach(function(mixOption, index) {
					const mixNumber = index + 1
					const label = (mixOption && mixOption.name) ? mixOption.name : ('Mix ' + mixNumber)
					this.mixList.push({ id: 'MIX' + mixNumber, label: label })
					if (mixOption && mixOption.sceneName) {
						const varUpdate = {}
						varUpdate['mix' + mixNumber + '_scene'] = mixOption.sceneName
						this.setVariableValues(varUpdate)
					}
					populatedMixNums.add(mixNumber)
				}.bind(this))

			} else if (resp && resp.mixes && Array.isArray(resp.mixes)) {
				// New format: { mixes: ["PROGRAM","MIX1",...], selected, currentProgramScene }
				resp.mixes.forEach(function(item) {
					var mixId, label
					if (typeof item === 'string') {
						mixId = item
					} else if (item && item.id) {
						mixId = item.id
					} else if (item && item.name) {
						mixId = item.name
					} else {
						return
					}

					if (item && item.name && item.name !== mixId) {
						label = item.name
					} else if (mixId === 'PROGRAM') {
						label = 'Program'
					} else {
						var numMatch = mixId.match(/\d+/)
						label = numMatch ? ('Mix ' + numMatch[0]) : mixId
					}

					this.mixList.push({ id: mixId, label: label })

					var numMatch2 = mixId.match(/\d+/)
					if (numMatch2) {
						var num = parseInt(numMatch2[0])
						if (num >= 1 && num <= 8) {
							populatedMixNums.add(num)
						}
					}
				}.bind(this))

				if (resp.selected) {
					this.states.activeMix = resp.selected
					this.setVariableValues({ active_mix: resp.selected })
					// console.log('[MIX-INIT] Active mix:', resp.selected)
				}
				if (resp.currentProgramScene !== undefined) {
					this.states.currentProgramScene = resp.currentProgramScene
					// console.log('[MIX-INIT] Current program scene:', resp.currentProgramScene)
				}

			} else {
				// console.log('[MIX-INIT] No mix options returned from server - will use placeholder mixes')
				this.mixList.push({ id: 'PROGRAM', label: 'Program' })
			}

		} catch (error) {
			// console.log('[MIX-INIT] Failed to get mix options:', error.message)
			this.log('debug', 'Failed to get mix options: ' + error.message)
			this.mixList.push({ id: 'PROGRAM', label: 'Program' })
		}

		// Fill placeholders for any MIX1-8 not returned by the server
		for (var i = 1; i <= 8; i++) {
			if (!populatedMixNums.has(i)) {
				this.mixList.push({ id: 'MIX' + i, label: 'Mix ' + i + ' (not available)' })
				var varUpdate = {}
				varUpdate['mix' + i + '_scene'] = 'None'
				this.setVariableValues(varUpdate)
				// console.log('[MIX-INIT] Added placeholder for Mix ' + i)
			}
		}

		// Sort: PROGRAM first, then MIX1-8 numerically
		var programEntry = null
		var otherEntries = []
		this.mixList.forEach(function(m) {
			if (m.id === 'PROGRAM') {
				programEntry = m
			} else {
				otherEntries.push(m)
			}
		})
		otherEntries.sort(function(a, b) {
			var numA = a.id.match(/\d+/)
			var numB = b.id.match(/\d+/)
			return (numA ? parseInt(numA[0]) : 0) - (numB ? parseInt(numB[0]) : 0)
		})
		if (programEntry) {
			this.mixList = [programEntry].concat(otherEntries)
		} else {
			this.mixList = otherEntries
		}

		// console.log('[MIX-INIT] Built mix list:', this.mixList)
		this.updateActionsFeedbacksVariables()
	}

	async refreshMixSceneStates() {
		try {
			const resp = await this.sendRequest('GetMixSceneOptions', {})
			const varUpdates = {}

			if (Array.isArray(resp)) {
				resp.forEach((mixOption, index) => {
					const mixNumber = index + 1
					if (mixOption && mixOption.sceneName) {
						this.states[`mix${mixNumber}Scene`] = mixOption.sceneName
						varUpdates[`mix${mixNumber}_scene`] = mixOption.sceneName
					}
				})
			} else if (resp && resp.mixes && Array.isArray(resp.mixes)) {
				if (resp.selected) {
					this.states.activeMix = resp.selected
					varUpdates['active_mix'] = resp.selected
				}
				resp.mixes.forEach((item) => {
					const mixId = typeof item === 'string' ? item : (item?.id || item?.name)
					if (!mixId) return
					const numMatch = mixId.match(/\d+/)
					if (numMatch) {
						const num = parseInt(numMatch[0])
						if (num >= 1 && num <= 8 && item?.sceneName) {
							this.states[`mix${num}Scene`] = item.sceneName
							varUpdates[`mix${num}_scene`] = item.sceneName
						}
					}
				})
			}

			if (Object.keys(varUpdates).length > 0) this.setVariableValues(varUpdates)
			this.checkFeedbacks('sceneMix')
		} catch (error) {
			this.log('debug', 'Failed to refresh mix scene states: ' + error.message)
		}
	}

	async buildZoomMeetingList() {
		this.zoom_meetingList = []

		try {
			const resp = await this.sendRequest('GetZoomMeetings', {})
			console.log('[ZOOM-INIT] GetZoomMeetings response:', resp)

			const meetings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.meetings) ? resp.meetings : [])

			meetings.forEach((meeting) => {
				const key = String(meeting.meetingNum)
				const label = meeting.authenticated && meeting.email
					? `Meeting ${key} (${meeting.email})`
					: `Meeting ${key}`
				this.zoom_meetingList.push({ id: key, label })
				this.states.zoomMeetings[key] = { active: !!(meeting.isActive) }
			})
		} catch (error) {
			console.log('[ZOOM-INIT] Failed to get zoom meetings:', error.message)
			this.log('debug', 'Failed to get zoom meetings: ' + error.message)
		}

		// Fallback: ensure 1-4 always available
		// if (this.zoom_meetingList.length === 0) {
		// 	for (let i = 1; i <= 4; i++) {
		// 		this.zoom_meetingList.push({ id: String(i), label: `Meeting ${i}` })
		// 	}
		// }

		console.log('[ZOOM-INIT] Built zoom meeting list:', this.zoom_meetingList)
		this.updateActionsFeedbacksVariables()
	}

	async refreshZoomMeetingStates() {
		try {
			const resp = await this.sendRequest('GetZoomMeetings', {})
			const meetings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.meetings) ? resp.meetings : [])
			meetings.forEach((meeting) => {
				const key = String(meeting.meetingNum)
				if (!this.states.zoomMeetings[key]) this.states.zoomMeetings[key] = {}
				this.states.zoomMeetings[key].active = !!(meeting.isActive)
			})
			this.checkFeedbacks('zoom_meeting_active')
		} catch (error) {
			console.log('[ZOOM] Failed to refresh zoom meeting states:', error.message)
		}
	}

	async buildNDIRecordingList() {
		this.NDI_recordingList = []

		try {
			const resp = await this.sendRequest('GetNDIFeeds', {})
			console.log('[NDI-INIT] GetNDIFeeds response:', resp)

			const recordings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.recordings) ? resp.recordings : [])

			const nameVars = {}
			recordings.forEach((recording) => {
				const key = String(recording.NDINum)
				const name = recording.name || recording.label || `NDI ${key}`
				this.NDI_recordingList.push({ id: key, label: `${key} - ${name}` })
				this.states.NDIRecordings[key] = { active: !!(recording.isActive), name }
				nameVars[`ndi_feed_${key}_name`] = name
				nameVars[`ndi_feed_${key}_active`] = !!(recording.isActive)
			})
			if (Object.keys(nameVars).length > 0) this.setVariableValues(nameVars)
		} catch (error) {
			console.log('[NDI-INIT] Failed to get NDI feeds:', error.message)
			this.log('debug', 'Failed to get NDI feeds: ' + error.message)
		}

		console.log('[NDI-INIT] Built NDI feed list:', this.NDI_recordingList)
		this.updateActionsFeedbacksVariables()
	}

	async refreshNDIRecordingStates() {
		try {
			const resp = await this.sendRequest('GetNDIFeeds', {})
			console.log('[NDI-REFRESH] GetNDIFeeds response:', JSON.stringify(resp))
			const recordings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.recordings) ? resp.recordings : [])
			const nameVars = {}
			recordings.forEach((recording) => {
				const key = String(recording.NDINum)
				const name = recording.name || recording.label || `NDI ${key}`
				console.log(`[NDI-REFRESH] Feed ${key}: isActive=${recording.isActive}`)
				if (!this.states.NDIRecordings[key]) this.states.NDIRecordings[key] = {}
				this.states.NDIRecordings[key].active = !!(recording.isActive)
				this.states.NDIRecordings[key].name = name
				nameVars[`ndi_feed_${key}_name`] = name
				nameVars[`ndi_feed_${key}_active`] = !!(recording.isActive)
			})
			if (Object.keys(nameVars).length > 0) this.setVariableValues(nameVars)
			this.checkFeedbacks('NDI_recording_active')
		} catch (error) {
			console.log('[NDI] Failed to refresh NDI feed states:', error.message)
		}
	}

	async buildISORecordingList() {
		this.ISO_recordingList = []

		try {
			const resp = await this.sendRequest('GetISOFeeds', {})
			console.log('[ISO-INIT] GetISOFeeds response:', resp)

			const recordings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.recordings) ? resp.recordings : [])

			const nameVars = {}
			recordings.forEach((recording) => {
				const key = String(recording.ISONum)
				const name = recording.name || recording.label || `ISO ${key}`
				this.ISO_recordingList.push({ id: key, label: `${key} \u2013 ${name}` })
				this.states.ISORecordings[key] = { active: !!(recording.isActive), name }
				nameVars[`iso_feed_${key}_name`] = name
				nameVars[`iso_feed_${key}_active`] = !!(recording.isActive)
			})
			if (Object.keys(nameVars).length > 0) this.setVariableValues(nameVars)
		} catch (error) {
			console.log('[ISO-INIT] Failed to get ISO feeds:', error.message)
			this.log('debug', 'Failed to get ISO feeds: ' + error.message)
		}

		console.log('[ISO-INIT] Built ISO feed list:', this.ISO_recordingList)
		this.updateActionsFeedbacksVariables()
	}

	async refreshISORecordingStates() {
		try {
			const resp = await this.sendRequest('GetISOFeeds', {})
			const recordings = Array.isArray(resp) ? resp : (resp && Array.isArray(resp.recordings) ? resp.recordings : [])
			const nameVars = {}
			recordings.forEach((recording) => {
				const key = String(recording.ISONum)
				const name = recording.name || recording.label || `ISO ${key}`
				if (!this.states.ISORecordings[key]) this.states.ISORecordings[key] = {}
				this.states.ISORecordings[key].active = !!(recording.isActive)
				this.states.ISORecordings[key].name = name
				nameVars[`iso_feed_${key}_name`] = name
				nameVars[`iso_feed_${key}_active`] = !!(recording.isActive)
			})
			if (Object.keys(nameVars).length > 0) this.setVariableValues(nameVars)
			this.checkFeedbacks('ISO_recording_active')
		} catch (error) {
			console.log('[ISO] Failed to refresh ISO feed states:', error.message)
		}
	}

	// MARK: Stats
	getStats() {
		this.cre8
			.call('GetStats')
			.then((data) => {
				this.states.stats = data

				let freeSpaceMB = this.roundNumber(data.availableDiskSpace, 0)
				let freeSpace = freeSpaceMB
				if (freeSpace > 1000) {
					freeSpace = `${this.roundNumber(freeSpace / 1000, 0)} GB`
				} else {
					freeSpace = `${this.roundNumber(freeSpace, 0)} MB`
				}

				this.setVariableValues({
					fps: this.roundNumber(data.activeFps, 2),
					render_total_frames: data.renderTotalFrames,
					render_missed_frames: data.renderSkippedFrames,
					output_total_frames: data.outputTotalFrames,
					output_skipped_frames: data.outputSkippedFrames,
					average_frame_time: this.roundNumber(data.averageFrameRenderTime, 2),
					cpu_usage: `${this.roundNumber(data.cpuUsage, 2)}%`,
					memory_usage: `${this.roundNumber(data.memoryUsage, 0)} MB`,
					free_disk_space: freeSpace,
					free_disk_space_mb: freeSpaceMB,
				})
				this.checkFeedbacks('freeDiskSpaceRemaining')
			})
			.catch((error) => {
				if (error?.message.match(/(Not connected)/i)) {
					this.connectionLost()
				}
			})
	}

	async getVideoSettings() {
		let videoSettings = await this.sendRequest('GetVideoSettings')
		
		if (videoSettings) {
			this.states.resolution = `${videoSettings.baseWidth}x${videoSettings.baseHeight}`
			this.states.outputResolution = `${videoSettings.outputWidth}x${videoSettings.outputHeight}`
			this.states.framerate = `${this.roundNumber(videoSettings.fpsNumerator / videoSettings.fpsDenominator, 2)} fps`
			this.setVariableValues({
				base_resolution: this.states.resolution,
				output_resolution: this.states.outputResolution,
				target_framerate: this.states.framerate,
			})
		}
	}
	
	// MARK: Outputs/Streams/Recs
	//Outputs, Streams, Recordings
	async getStreamStatus() {
		let streamStatus = await this.sendRequest('GetStreamStatus')
		let streamService = await this.sendRequest('GetStreamServiceSettings')

		if (streamStatus) {
			this.states.streaming = streamStatus.outputActive
			this.states.streamingTimecode = streamStatus.outputTimecode.match(/\d\d:\d\d:\d\d/i)
			this.states.streamCongestion = streamStatus.outputCongestion

			let kbits = 0
			if (streamStatus.outputBytes > this.states.outputBytes) {
				kbits = Math.round(((streamStatus.outputBytes - this.states.outputBytes) * 8) / 1000)
				this.states.outputBytes = streamStatus.outputBytes
			} else {
				this.states.outputBytes = streamStatus.outputBytes
			}

			this.checkFeedbacks('streaming', 'streamCongestion')
			this.setVariableValues({
				streaming: streamStatus.outputActive ? 'Live' : 'Off-Air',
				stream_timecode: this.states.streamingTimecode,
				output_skipped_frames: streamStatus.outputSkippedFrames,
				output_total_frames: streamStatus.outputTotalFrames,
				kbits_per_sec: kbits,
				stream_service: streamService?.streamServiceSettings?.service ?? 'Custom',
			})
		}
	}

	async getRecordStatus() {
		let recordStatus = await this.sendRequest('GetRecordStatus')
		let recordDirectory = await this.sendRequest('GetRecordDirectory')

		if (recordStatus) {
			if (recordStatus.outputActive === true) {
				this.states.recording = 'Recording'
			} else {
				this.states.recording = recordStatus.outputPaused ? 'Paused' : 'Stopped'
			}

			this.states.recordingTimecode = recordStatus.outputTimecode.match(/\d\d:\d\d:\d\d/i)
			this.states.recordDirectory = recordDirectory.recordDirectory

			this.checkFeedbacks('recording')
			this.setVariableValues({
				recording: this.states.recording,
				recording_timecode: this.states.recordingTimecode,
				recording_path: this.states.recordDirectory,
			})
		}
	}

	async getOutputStatus(outputName) {
		if (!this.states.sceneCollectionChanging) {
			let outputStatus = await this.sendRequest('GetOutputStatus', { outputName: outputName })
			this.outputs[outputName] = outputStatus
			this.checkFeedbacks('output_active')
		}
	}

	async getReplayBufferStatus() {
		let replayBuffer = await this.sendRequest('GetReplayBufferStatus')

		if (replayBuffer) {
			this.states.replayBuffer = replayBuffer.outputActive
			this.checkFeedbacks('replayBufferActive')
		}
	}

	// MARK: Scene Collect/build
	async buildSceneList() {
		this.scenes = []
		this.sceneChoices = []

		let sceneList = await this.sendRequest('GetSceneList')

		if (sceneList) {
			this.scenes = sceneList.scenes
			this.states.previewScene = sceneList.currentPreviewSceneName ?? 'None'
			this.states.programScene = sceneList.currentProgramSceneName

			this.setVariableValues({
				scene_preview: this.states.te,
				scene_active: this.states.programScene,
			})

			this.scenes.forEach((scene) => {
				let sceneName = scene.sceneName
				this.sceneChoices.push({ id: sceneName, label: sceneName })
				this.buildSourceList(sceneName)
			})
			
			// Get vCam AUX options to populate vcamSceneList
			const vcamOptions = await this.sendRequest('GetVcamAuxOptions', {})
			if (vcamOptions?.sceneList && vcamOptions.sceneList.length > 0) {
				this.vcamSceneList = vcamOptions.sceneList.map(scene => ({ id: scene, label: scene }))
				// Add Program as the first scene option (program output)
				this.vcamSceneList.unshift({ id: 'Program', label: 'Program' })
				// add None as the initial option
				this.vcamSceneList.unshift({ id: 'None', label: 'None' })
				this.log('info', `Loaded ${this.vcamSceneList.length} scenes for vCam dropdown (including Program)`)
			} else {
				// Fallback: use sceneChoices if vcamOptions.sceneList is not available
				this.vcamSceneList = [...this.sceneChoices]
				this.vcamSceneList.unshift({ id: 'Program', label: 'Program' })
				this.vcamSceneList.unshift({ id: 'None', label: 'None' })
				this.log('info', `Using sceneChoices fallback for vCam dropdown: ${this.vcamSceneList.length} scenes`)
			}
			if (vcamOptions?.auxList && vcamOptions.auxList.length > 0) {
				this.auxAudioList = vcamOptions.auxList.map(aux => ({ id: aux, label: aux }))
				this.log('info', `Loaded ${this.auxAudioList.length} aux mixes from StudioPro`)
			} else {
				this.log('warn', `No aux mixes returned from StudioPro - auxAudioList will be empty`)
			}
			
			this.updateActionsFeedbacksVariables()
		}
	}

	// MARK: Scene Item Builds
	async buildSourceList(sceneName) {
		let data = await this.sendRequest('GetSceneItemList', { sceneName: sceneName })

		if (data) {
			this.sceneItems[sceneName] = data.sceneItems

			let batch = []
			for (const sceneItem of data.sceneItems) {
				let sourceName = sceneItem.sourceName
				this.sources[sourceName] = sceneItem

				//Generate name that can be used as valid Variable IDs
				this.sources[sourceName].validName = this.validName(sceneItem.sourceName)

				if (!this.sourceChoices.find((item) => item.id === sourceName)) {
					this.sourceChoices.push({ id: sourceName, label: sourceName })
				}

				if (sceneItem.isGroup) {
					this.getGroupInfo(sourceName)
				}

				batch.push(
					{
						requestId: sourceName,
						requestType: 'GetSourceActive',
						requestData: { sourceName: sourceName },
					},
					{
						requestId: sourceName,
						requestType: 'GetSourceFilterList',
						requestData: { sourceName: sourceName },
					}
				)
				if (sceneItem.inputKind) {
					batch.push({
						requestId: sourceName,
						requestType: 'GetInputSettings',
						requestData: { inputName: sourceName },
					})
				}
				this.getAudioSources(sourceName)
			}

			let sourceBatch = await this.sendBatch(batch)

			if (sourceBatch) {
				for (const response of sourceBatch) {
					if (response.requestStatus.result) {
						let sourceName = response.requestId
						let type = response.requestType
						let data = response.responseData

						switch (type) {
							case 'GetSourceActive':
								this.sources[sourceName].active = data.videoActive
								this.sources[sourceName].videoShowing = data.videoShowing
								break
							case 'GetSourceFilterList':
								this.sourceFilters[sourceName] = data.filters
								if (data?.filters) {
									this.sourceFilters[sourceName] = data.filters
									data.filters.forEach((filter) => {
										if (!this.filterList.find((item) => item.id === filter.filterName)) {
											this.filterList.push({ id: filter.filterName, label: filter.filterName })
										}
									})
								}
								break
							case 'GetInputSettings':
								this.buildInputSettings(sourceName, data.inputKind, data.inputSettings)
								break
							default:
								break
						}
					}
				}
				this.checkFeedbacks('scene_item_active')
				this.updateActionsFeedbacksVariables()
			}
		}
	}

	async getGroupInfo(groupName) {
		let data = await this.sendRequest('GetGroupSceneItemList', { sceneName: groupName })
		if (data) {
			this.groups[groupName] = data.sceneItems
			data.sceneItems?.forEach(async (sceneItem) => {
				let sourceName = sceneItem.sourceName
				this.sources[sourceName] = sceneItem
				this.sources[sourceName].validName = this.validName(sourceName)

				//Flag that this source is part of a group
				this.sources[sourceName].groupedSource = true
				this.sources[sourceName].groupName = groupName

				if (!this.sourceChoices.find((item) => item.id === sourceName)) {
					this.sourceChoices.push({ id: sourceName, label: sourceName })
				}

				this.getSourceFilters(sourceName)
				this.getAudioSources(sourceName)

				if (sceneItem.inputKind) {
					let input = await this.sendRequest('GetInputSettings', { inputName: sourceName })

					if (input.inputSettings) {
						this.buildInputSettings(sourceName, sceneItem.inputKind, input.inputSettings)
						this.updateActionsFeedbacksVariables()
					}
				}
			})
			this.updateActionsFeedbacksVariables()
		}
	}

	async buildSceneTransitionList() {
		this.transitionList = []

		let sceneTransitionList = await this.sendRequest('GetSceneTransitionList')
		let currentTransition = await this.sendRequest('GetCurrentSceneTransition')

		if (sceneTransitionList) {
			sceneTransitionList.transitions?.forEach((transition) => {
				this.transitionList.push({ id: transition.transitionName, label: transition.transitionName })
			})

			this.states.currentTransition = currentTransition?.transitionName ?? 'None'
			this.states.transitionDuration = currentTransition?.transitionDuration ?? '0'

			this.checkFeedbacks('transition_duration', 'current_transition')
			this.setVariableValues({
				current_transition: this.states.currentTransition,
				transition_duration: this.states.transitionDuration,
				transition_active: 'False',
			})
		}
	}

	//Scene and Source Actions
	addScene(sceneName) {
		this.sceneChoices.push({ id: sceneName, label: sceneName })
		this.buildSourceList(sceneName)
		this.updateActionsFeedbacksVariables()
	}

	removeScene(sceneName) {
		let scene = this.sceneChoices.findIndex((item) => item.id === sceneName)
		if (scene) {
			this.sceneChoices.splice(scene, 1)
		}
		delete this.sceneItems[sceneName]
		this.updateActionsFeedbacksVariables()
	}

	//Source Info
	async getMediaStatus() {
		let batch = []
		for (const source of this.mediaSourceList) {
			let sourceName = source.id
			batch.push({
				requestId: sourceName,
				requestType: 'GetMediaPlayerInputStatus', 
				requestData: { inputName: sourceName },
			})
		}

		let data = await this.sendBatch(batch)
		let fallbackBatch = []

		if (data) {
			for (const response of data) {
				let sourceName = response.requestId
				let validName = this.sources[sourceName].validName ?? sourceName

				if (response.requestStatus.result) {
					let data = response.responseData
					this.mediaSources[sourceName] = data

					let remaining = data?.mediaDuration - data?.mediaCursor
					if (remaining > 0) {
						remaining = this.formatTimecode(remaining)
					} else {
						remaining = '--:--:--'
					}

					this.mediaSources[sourceName].timeElapsed = this.formatTimecode(data.mediaCursor)
					this.mediaSources[sourceName].timeRemaining = remaining

					// enhanced playlist variables
					if (data?.playlist && Array.isArray(data.playlist)) {
						const playlistCount = data.playlist.length
						const currentIndex = data.mediaCurrentIndex ?? 0
						const currentClip = data.playlist[currentIndex]
						
						this.setVariableValues({
							[`media_playlist_count_${validName}`]: playlistCount,
							[`media_current_clip_index_${validName}`]: currentIndex,
							[`media_current_clip_name_${validName}`]: currentClip?.displayName || currentClip?.path?.split(/[\\/]/).pop() || 'Unknown',
							[`media_loop_state_${validName}`]: data.mediaLoop ? 'true' : 'false',
							[`media_playlist_loop_state_${validName}`]: data.playlistLoop ? 'true' : 'false',
							[`media_auto_advance_${validName}`]: data.autoAdvance ? 'true' : 'false',
						})
					}

					if (data?.mediaState) {
						switch (data?.mediaState) {
							case 'CRE8_MEDIA_STATE_PLAYING':
								this.setVariableValues({
									current_media_name: sourceName,
									current_media_time_elapsed: this.mediaSources[sourceName].timeElapsed,
									current_media_time_remaining: this.mediaSources[sourceName].timeRemaining,
									[`media_status_${validName}`]: 'Playing',
								})
								break
							case 'CRE8_MEDIA_STATE_PAUSED':
								this.setVariableValues({ [`media_status_${validName}`]: 'Paused' })
								break
							default:
								this.setVariableValues({ [`media_status_${validName}`]: 'Stopped' })
								break
						}
					}
					this.setVariableValues({
						[`media_time_elapsed_${validName}`]: this.mediaSources[sourceName].timeElapsed,
						[`media_time_remaining_${validName}`]: remaining,
					})
					this.checkFeedbacks('media_playing', 'media_source_time_remaining', 'playlist_loop_active', 'item_loop_active', 'auto_advance_active')
				} else {
					// fallback to standard command if enhanced not available
					fallbackBatch.push({
						requestId: sourceName,
						requestType: 'GetMediaInputStatus',
						requestData: { inputName: sourceName },
					})
				}
			}
		}

		// process fallback for standard media inputs
		if (fallbackBatch.length > 0) {
			let fallbackData = await this.sendBatch(fallbackBatch)
			
			if (fallbackData) {
				for (const response of fallbackData) {
					if (response.requestStatus.result) {
						let sourceName = response.requestId
						let validName = this.sources[sourceName].validName ?? sourceName
						let data = response.responseData

						this.mediaSources[sourceName] = data

						let remaining = data?.mediaDuration - data?.mediaCursor
						if (remaining > 0) {
							remaining = this.formatTimecode(remaining)
						} else {
							remaining = '--:--:--'
						}

						this.mediaSources[sourceName].timeElapsed = this.formatTimecode(data.mediaCursor)
						this.mediaSources[sourceName].timeRemaining = remaining

						if (data?.mediaState) {
							switch (data?.mediaState) {
								case 'CRE8_MEDIA_STATE_PLAYING':
									this.setVariableValues({
										current_media_name: sourceName,
										current_media_time_elapsed: this.mediaSources[sourceName].timeElapsed,
										current_media_time_remaining: this.mediaSources[sourceName].timeRemaining,
										[`media_status_${validName}`]: 'Playing',
									})
									break
								case 'CRE8_MEDIA_STATE_PAUSED':
									this.setVariableValues({ [`media_status_${validName}`]: 'Paused' })
									break
								default:
									this.setVariableValues({ [`media_status_${validName}`]: 'Stopped' })
									break
							}
						}
						this.setVariableValues({
							[`media_time_elapsed_${validName}`]: this.mediaSources[sourceName].timeElapsed,
							[`media_time_remaining_${validName}`]: remaining,
						})

						// Update media tab status if this source is assigned to a tab
						this.updateMediaTabStatus(sourceName, data.mediaState)

						this.checkFeedbacks('media_playing', 'media_source_time_remaining')
					}
				}
			}
		}
	}

	updateMediaTabStatus(sourceName, mediaState) {
		// Check which tab (if any) has this source assigned
		for (let i = 1; i <= 5; i++) {
			let tabSourceVar = `media_tab_${i}_source`
			let assignedSource = this.getVariableValue(tabSourceVar)
			
			if (assignedSource === sourceName) {
				// Update tab status based on media state
				let tabStatus = 'Stopped'
				switch (mediaState) {
					case 'CRE8_MEDIA_STATE_PLAYING':
						tabStatus = 'Playing'
						break
					case 'CRE8_MEDIA_STATE_PAUSED':
						tabStatus = 'Paused'
						break
					default:
						tabStatus = 'Stopped'
						break
				}
				
				this.setVariableValues({
					[`media_tab_${i}_status`]: tabStatus
				})
				
				// Check feedbacks for this specific tab and active tab
				this.checkFeedbacks('media_tab_playing', 'media_tab_status', 'active_media_tab_playing')
				break // Source can only be assigned to one tab
			}
		}
	}

	buildInputSettings(sourceName, inputKind, inputSettings) {
		let name = this.sources[sourceName].validName ?? sourceName
		this.sources[sourceName].settings = inputSettings

		switch (inputKind) {
			case 'text_ft2_source_v2':
			case 'text_gdiplus_v2':
				//Exclude text sources that read from file, as there is no way to edit or read the text value
				if (inputSettings?.text) {
					this.textSourceList.push({ id: sourceName, label: sourceName })
					this.setVariableValues({
						[`current_text_${name}`]: inputSettings.text ?? '',
					})
				} else if (inputSettings?.from_file) {
					this.setVariableValues({
						[`current_text_${name}`]: `Text from file: ${inputSettings.text_file}`,
					})
				}
				break
			case 'vlc_source':
				// Only VLC sources are added to mediaSourceList for media tab control
				this.log('debug', `Adding to mediaSourceList: ${sourceName} (inputKind: ${inputKind})`)
				this.mediaSourceList.push({ id: sourceName, label: sourceName })
				if (!this.mediaPoll) {
					this.startMediaPoll()
				}
				break
			case 'ffmpeg_source':
			case 'media_source':
				// Other media sources start media poll but are not added to mediaSourceList
				if (!this.mediaPoll) {
					this.startMediaPoll()
				}
				break
			case 'image_source':
				this.imageSourceList.push({ id: sourceName, label: sourceName })
				break
			default:
				break
		}
	}

	updateInputSettings(sourceName, inputSettings) {
		if (this.sources[sourceName]) {
			this.sources[sourceName].settings = inputSettings
			let name = this.sources[sourceName].validName ?? sourceName
			let inputKind = this.sources[sourceName].inputKind

			switch (inputKind) {
				case 'text_ft2_source_v2':
				case 'text_gdiplus_v2':
					//Exclude text sources that read from file, as there is no way to edit or read the text value
					if (inputSettings?.text) {
						this.setVariableValues({
							[`current_text_${name}`]: inputSettings.text ?? '',
						})
					} else if (inputSettings?.from_file) {
						this.setVariableValues({
							[`current_text_${name}`]: `Text from file: ${inputSettings.text_file}`,
						})
					}
					break
				case 'ffmpeg_source':
				case 'vlc_source':
					let file = ''
					if (inputSettings?.playlist) {
						file = inputSettings?.playlist[0]?.value?.match(/[^\\\/]+(?=\.[\w]+$)|[^\\\/]+$/)
						//Use first value in playlist until support for determining currently playing cue
					} else if (inputSettings?.local_file) {
						file = inputSettings?.local_file?.match(/[^\\\/]+(?=\.[\w]+$)|[^\\\/]+$/)
					}
					this.setVariableValues({ [`media_file_name_${name}`]: file })

					break
				case 'image_source':
					this.setVariableValues({
						[`image_file_name_${name}`]: inputSettings?.file
							? inputSettings?.file?.match(/[^\\\/]+(?=\.[\w]+$)|[^\\\/]+$/)
							: '',
					})
					break
				default:
					break
			}
		}
	}

	async getSourceFilters(sourceName) {
		let data = await this.sendRequest('GetSourceFilterList', { sourceName: sourceName })

		if (data?.filters) {
			this.sourceFilters[sourceName] = data.filters
			data.filters.forEach((filter) => {
				if (!this.filterList.find((item) => item.id === filter.filterName)) {
					this.filterList.push({ id: filter.filterName, label: filter.filterName })
					this.updateActionsFeedbacksVariables()
				}
			})
		}
	}

	//Audio Sources
	getAudioSources(sourceName) {
		this.cre8
			.call('GetInputAudioTracks', { inputName: sourceName })
			.then((data) => {
				if (!this.audioSourceList.find((item) => item.id === sourceName)) {
					this.audioSourceList.push({ id: sourceName, label: sourceName })
					this.sources[sourceName].inputAudioTracks = data.inputAudioTracks
					this.getSourceAudio(sourceName)
					this.updateActionsFeedbacksVariables()
				}
			})
			.catch((error) => {
				//Ignore, this source is not an audio source
			})
	}

	async getSourceAudio(sourceName) {
		// this.log('debug', `Getting audio info for ${sourceName}`)
		let validName = this.validName(sourceName)

		let batch = [
			{
				requestId: sourceName,
				requestType: 'GetInputMute',
				requestData: { inputName: sourceName },
			},
			{
				requestId: sourceName,
				requestType: 'GetInputVolume',
				requestData: { inputName: sourceName },
			},
			{
				requestId: sourceName,
				requestType: 'GetInputAudioBalance',
				requestData: { inputName: sourceName },
			},
			{
				requestId: sourceName,
				requestType: 'GetInputAudioSyncOffset',
				requestData: { inputName: sourceName },
			},
			{
				requestId: sourceName,
				requestType: 'GetInputAudioMonitorType',
				requestData: { inputName: sourceName },
			},
			{
				requestId: sourceName,
				requestType: 'GetInputAudioTracks',
				requestData: { inputName: sourceName },
			},
		]

		let data = await this.sendBatch(batch)

		for (const response of data) {
			if (response.requestStatus.result && response.responseData) {
				let sourceName = response.requestId
				let type = response.requestType
				let data = response.responseData

				switch (type) {
					case 'GetInputMute':
						this.sources[sourceName].inputMuted = data.inputMuted
						break
					case 'GetInputVolume':
						this.sources[sourceName].inputVolume = this.roundNumber(data.inputVolumeDb, 1)
						break
					case 'GetInputAudioBalance':
						this.sources[sourceName].inputAudioBalance = this.roundNumber(data.inputAudioBalance, 1)
						break
					case 'GetInputAudioSyncOffset':
						this.sources[sourceName].inputAudioSyncOffset = data.inputAudioSyncOffset
						break
					case 'GetInputAudioMonitorType':
						this.sources[sourceName].monitorType = data.monitorType
						let monitorType
						if (data.monitorType === 'CRE8_MONITORING_TYPE_MONITOR_AND_OUTPUT') {
							monitorType = 'Monitor / Output'
						} else if (data.monitorType === 'CRE8_MONITORING_TYPE_MONITOR_ONLY') {
							monitorType = 'Monitor Only'
						} else {
							monitorType = 'Off'
						}
						this.setVariableValues({ [`monitor_${validName}`]: monitorType })
						break
					case 'GetInputAudioTracks':
						this.sources[sourceName].inputAudioTracks = data.inputAudioTracks
						break
					default:
						break
				}
			}
		}

		this.setVariableValues({
			[`mute_${validName}`]: this.sources[sourceName].inputMuted ? 'Muted' : 'Unmuted',
			[`volume_${validName}`]: this.sources[sourceName].inputVolume + 'dB',
			[`balance_${validName}`]: this.sources[sourceName].inputAudioBalance,
			[`sync_offset_${validName}`]: this.sources[sourceName].inputAudioSyncOffset + 'ms',
		})
		this.checkFeedbacks('audio_muted', 'volume', 'audio_monitor_type')
	}

	updateAudioPeak(data) {
		this.audioPeak = {}
		data.inputs.forEach((input) => {
			let channel = input.inputLevelsMul[0]
			if (channel) {
				let channelPeak = channel?.[1]
				let dbPeak = Math.round(20.0 * Math.log10(channelPeak))
				if (this.audioPeak && dbPeak) {
					this.audioPeak[input.inputName] = dbPeak
					this.checkFeedbacks('audioPeaking', 'audioMeter')
				}
			}
		})
	}

	//DSk tab list
	async buildDSKTabs() {
		this.dskTabChoices = []
		this.dskItemChoices = []
		
		console.log('Building DSK tabs...');
		let dskTabs = await this.sendRequest('CallVendorRequest',  {vendorName: "downstream-keyer", requestType: 'get_dsk_tabs'})
		if (dskTabs && dskTabs.responseData && dskTabs.responseData.success) {
			const tabCount = dskTabs.responseData.tabCount;
			console.log(`DSK tab count from server: ${tabCount}`);
			
			for (let i = 0; i < tabCount; i ++) {
				this.dskTabChoices.push({id: (i+1).toString(), label: `Tab ${i+1}`}); 
			}
			
			console.log('DSK tab choices built:', this.dskTabChoices);
			
			// Load items for the first tab by default
			if (tabCount > 0) {
				console.log('Loading default DSK items for tab 0');
				await this.buildDSKItems(0);
			}
		} else {
			console.log('Failed to get DSK tabs or no tabs available:', dskTabs);
		}

		this.updateActionsFeedbacksVariables();
		
		// Update all DSK name variables after building tabs
		this.updateAllDSKNameVariables();
		
		console.log('DSK tabs build complete, updating actions/feedbacks/variables');
	}

	async buildDSKItems(dskTabIdx) {
		let data = await this.sendRequest('CallVendorRequest', {vendorName: "downstream-keyer", requestType: 'get_dsk_items_in_tab', requestData: {'dskTabIdx': dskTabIdx}})
		
		console.log('Raw DSK data received:', data);
		
		// Initialise per-tab storage
		if (!this.dskItemChoicesByTab) {
			this.dskItemChoicesByTab = {};
		}
		this.dskItemChoicesByTab[dskTabIdx] = [];
		
		// Store DSK states for potential feedback use
		if (!this.dskStates) {
			this.dskStates = {}
		}
		if (!this.dskStates[dskTabIdx]) {
			this.dskStates[dskTabIdx] = {}
		}
		
		if (data && data.responseData && data.responseData.success && data.responseData.dskData) {
			const dskData = data.responseData.dskData;
			console.log('Processing DSK data:', dskData);
			for (const dskItem of dskData) {
				console.log('Processing DSK item:', dskItem);
				
				// Parse the sceneName which contains "SceneName;state"
				const parts = dskItem.sceneName.split(';');
				const actualSceneName = parts[0];
				const isEnabled = parts[1] === 'true';
				
				console.log('Parsed DSK item:', {
					original: dskItem.sceneName,
					actualSceneName,
					isEnabled
				});
				
				// Store state for this DSK item
				this.dskStates[dskTabIdx][actualSceneName] = isEnabled;

				// Add to tab-specific choices
				if (!this.dskItemChoicesByTab[dskTabIdx].find((item) => item.id === actualSceneName)) {
					const newItem = { 
						id: actualSceneName, 
						label: actualSceneName,
						// Store the rest of teh data
						json: JSON.stringify({
							sceneName: actualSceneName,
							enabled: isEnabled,
							tabIndex: dskTabIdx
						})
					};
					console.log('Adding DSK item to choices:', newItem);
					this.dskItemChoicesByTab[dskTabIdx].push(newItem);
				}
			}
		} else {
			console.log('Invalid or empty DSK data received:', data);
		}

		// update global list
		if (dskTabIdx === 0) {
			this.dskItemChoices = this.dskItemChoicesByTab[0] || [];
			console.log('Updated global DSK choices:', this.dskItemChoices);
		}

		console.log(`Final DSK Items for tab ${dskTabIdx}:`, this.dskItemChoicesByTab[dskTabIdx]);
		
		// Update DSK name variable for this tab
		this.updateDSKNameVariable(dskTabIdx);
		
		this.updateActionsFeedbacksVariables();
		// Check DSK feedbacks after building items
		this.checkFeedbacks('dsk_tab_active', 'dsk_display_active');
		// Update DSK display variable
		this.updateDSKDisplay();
	}

	getAuxIdxFromName(auxName) {
		if (auxName === "PGM") return 0;
		else if (auxName === "AUX1") return 2;
		else if (auxName === "AUX2") return 3;
		else if (auxName === "AUX3") return 4;
		else if (auxName === "AUX4") return 5;
		else if (auxName === "AUX5") return 6;
		else if (auxName === "AUX6") return 7;
		else if (auxName === "AUX7") return 8;
		else if (auxName === "AUX8") return 9;

		return -1;
	}

	updateDSKVariables() {
		// Update DSK variable values based on actual DSK data
		const variableValues = {};
		
		for (const tabIdx in this.dskStates) {
			const tabNumber = parseInt(tabIdx) + 1; // Convert 0-based to 1-based 
			
			for (const dskItemName in this.dskStates[tabIdx]) {
				const isEnabled = this.dskStates[tabIdx][dskItemName];
				const status = isEnabled ? 'ON' : 'OFF';
				
				// Create variable ID based on DSK item name and tab
				const validItemName = this.validName(dskItemName); // Make valid for variable ID
				const variableId = `dsk_tab_${tabNumber}_${validItemName}`;
				
				// Set the variable value to show "DSK Name: Status"
				variableValues[variableId] = `${dskItemName}: ${status}`;
			}
		}
		
		// Update variable values
		if (Object.keys(variableValues).length > 0) {
			this.setVariableValues(variableValues);
			console.log('Updated DSK variable values:', variableValues);
		}
	}

	updateDSKNameVariable(dskTabIdx) {
		// Update DSK name variable for the specified tab (following same pattern as scene_preview)
		const tabNumber = dskTabIdx + 1; // Convert 0-based to 1-based
		const variableId = `dsk_tab_${tabNumber}_name`;
		
		console.log(`Updating DSK name variable for tab ${dskTabIdx} (variable: ${variableId})`);
		
		// Get the first DSK item name in this tab (assuming one per tab)
		const tabItems = this.dskItemChoicesByTab[dskTabIdx] || [];
		console.log('Available DSK items for tab:', tabItems);
		
		if (tabItems.length > 0) {
			const dskItem = tabItems[0];
			console.log('Using first DSK item:', dskItem);
			
			// Parse the JSON data to get the actual scene name
			try {
				console.log('Attempting to parse JSON data:', dskItem.json);
				const itemData = JSON.parse(dskItem.json);
				console.log('Parsed JSON data:', itemData);
				
				const sceneName = itemData.sceneName || dskItem.id;
				console.log(`Setting variable ${variableId} to scene name:`, sceneName);
				
				this.setVariableValues({ [variableId]: sceneName });
				console.log(`Successfully updated ${variableId} to scene name: ${sceneName}`);
				
				// Log how to reference this variable with correct capitalization
				console.log(`and this is how the variable should be referenced: $(StudioPro:${variableId})`);
				
				// Test getting the variable value
				const directValue = this.getVariableValue(variableId);
				console.log('Variable value test:', {
					variableId,
					directValue
				});
				
			} catch (e) {
				// If JSON parsing fails, use the ID as fallback
				console.log('JSON parsing failed:', e.message);
				console.log(`Falling back to ID for ${variableId}:`, dskItem.id);
				
				this.setVariableValues({ [variableId]: dskItem.id });
				console.log(`Updated ${variableId} to ID: ${dskItem.id} (JSON parse failed:`, e.message, ')');
			}
		} else {
			console.log(`No DSK items found for tab ${dskTabIdx}, clearing ${variableId}`);
			this.setVariableValues({ [variableId]: '' });
		}
		
		// Log the final variable value
		const finalValue = this.getVariableValue(variableId);
		console.log(`Final value of ${variableId}:`, finalValue);
	}

	updateAllDSKNameVariables() {
		// Update all DSK name variables for loaded tabs
		console.log('Updating all DSK name variables...');
		if (this.dskItemChoicesByTab) {
			for (const tabIdx in this.dskItemChoicesByTab) {
				const tabIndex = parseInt(tabIdx);
				if (!isNaN(tabIndex)) {
					this.updateDSKNameVariable(tabIndex);
				}
			}
		}
	}

	audioControlKnob(sourceIdx, direction) {
		console.log('The sourceIdx ', sourceIdx, ' direction ', direction);
		console.log('The ip 1 i ', this.getVariableValue('ip_1'));
		
		const sourceVarKeys = ['audio_control_source_1', 'audio_control_source_2', 'audio_control_source_3', 'audio_control_source_4'];
		const controlTypeVarKeys = ['audio_control_type_1', 'audio_control_type_2', 'audio_control_type_3', 'audio_control_type_4'];
		const ipVarKeys = ['ip_1', 'ip_2', 'ip_3', 'ip_4'];

		const controlTypeKey = this.getVariableValue(controlTypeVarKeys[sourceIdx]);
		const key = sourceVarKeys[sourceIdx];
		const ipkey = ipVarKeys[sourceIdx];
		console.log('The key is ', controlTypeKey );
		const audioSourceList = this.audioSourceList.concat(this.auxAudioList);

		if(controlTypeKey === 0)
		{
			let currentValue = this.getVariableValue(ipkey);
			if(currentValue > 255)
			{
				currentValue = 255;
			}
			else if(currentValue < 1)
			{
				currentValue = 1;
			} 

			this.setVariableValues({[ipkey]: parseInt(currentValue) + direction});
			
			// Save IP settings when changed
			this.log('debug', `IP value changed for ${ipkey}, saving settings`);
			this.saveAudioControlSettings().catch(error => {
				this.log('error', `Failed to save audio control settings: ${error.message}`)
			})

		}else if (controlTypeKey === 1) { // Source selection
			let value = this.getVariableValue(key);
			if (!value) value = "";
			if (!audioSourceList || audioSourceList.length === 0) return;
			
			let valueIndex = -1;
			for (let i = 0; i < audioSourceList.length; i ++) {
				if (value === audioSourceList[i].id) valueIndex = i;
			}
			if (valueIndex === undefined || valueIndex === null || valueIndex < 0) {
				valueIndex = 0;
			}else{
				valueIndex = valueIndex + 1 * direction;
				if (valueIndex < 0) valueIndex = audioSourceList.length - 1;
				if (valueIndex >= audioSourceList.length) valueIndex = 0;
			}

			const newValue = audioSourceList[valueIndex] ? audioSourceList[valueIndex].id : "";
			this.setVariableValues({[key]: newValue});
			
			// Save the selection when a source is selected (only if not empty)
			if (newValue && newValue !== '') {
				this.log('debug', `Source selected: ${newValue} for source index ${sourceIdx}, saving settings`);
				this.saveAudioControlSettings().catch(error => {
					this.log('error', `Failed to save audio control settings: ${error.message}`)
				})
			}
		}else if (controlTypeKey === 2) { // audio volume control
			const sourceName = this.getVariableValue(key);
			console.log('The sourcename is ', sourceName);
			
			if (sourceName.indexOf("AUX") >= 0 || sourceName === "PGM"){
				const trackIdx = this.getAuxIdxFromName(sourceName);
				this.sendRequest('CallVendorRequest',  {vendorName: "cre8-app-main-controls", 
					requestType: 'set_track_volume_byinc', requestData: {"track-idx": trackIdx, "increment": direction * 100}}) // 1000 --> 1, 100 --->0.1
				return;
			} // pggy-backing this request for action: program_volume_control

			if (sourceName){
				let newVolume = this.sources[sourceName].inputVolume + 0.1 * direction // 5 is increment/decrement of voume
				if (newVolume > 20) {
					newVolume = 20
				} else if (newVolume < -60) {
					newVolume = -60.1
				}

				this.sendRequest('SetInputVolume', { inputName: sourceName, inputVolumeDb: newVolume })
				this.log('debug', `Setting volume of ${sourceName} to ${newVolume}`) // TODO: this is where we get the dB values for the adjust_volume knobs
			}
		}
	}

	async toggleIP(type){
		console.log('ToggleIP');
		let settings = this.getVariableValue('ipSettings');
		console.log('The type  is ', type);
		let address = this.getVariableValue(type);
		console.log('the address is ', address);
		const ipVarKeys = ['ip_1', 'ip_2', 'ip_3', 'ip_4'];
		settings = !settings;
		this.setVariableValues({['ipSettings']:settings});

		if(settings)
		{
			try {
				const { stdout, stderr } = await exec1('hostname -I');
				//console.log( 'the stdout is', stdout );
				let currentIP = address;
				let IP = currentIP.split('.');
				for(let i = 0;i<4;i++)
				{
				console.log('The ip key is ', ipVarKeys[i] , 'the ip number is ', IP[i])
				this.setVariableValues({[ ipVarKeys[i]]: parseInt(IP[i])});

				}
				//console.log( 'the stderr is', stderr );
			} catch (error) {
				// this.log('error', 'Failed to run terminal command:', error.message);
				return;
			}
		}
		else{
			for(let i = 0;i<4;i++)
			{
				console.log('Setting ip to emptystring')
				this.setVariableValues({[ ipVarKeys[i]]: ''});

			}

		}

		// Save IP settings when toggled
		this.log('debug', `IP settings toggled, saving settings`);
		this.saveAudioControlSettings().catch(error => {
			this.log('error', `Failed to save audio control settings: ${error.message}`)
		})

	

	}

	updateAudioControlSourceType(key) {
		
		let value = this.getVariableValue(key);
		value ++;
		if (value > 2) value = 0;

		this.setVariableValues({[key]: value});
		this.checkFeedbacks('audio_control_type')
		
		// Auto-save settings when control type changes
		this.log('debug', `Control type changed for ${key} to ${value}, saving settings`);
		this.saveAudioControlSettings().catch(error => {
			this.log('error', `Failed to save audio control settings: ${error.message}`)
		})
	}

	async updateNetworkMethod(){
		
		try {
			const { stdout, stderr } = await exec1('nmcli con show eth | grep ipv4.method');
			console.log('The method is ', stdout.split(':')[1].trim());
		} catch (error) {
			// this.log('error', 'Failed to run terminal command:', error.message);
			return;
		}

	}
	async setStatic(){
		
		try {
			const { stdout, stderr } = await exec1('nmcli con mod eth ipv4.method manual');
		} catch (error) {
			// this.log('error', 'Failed to run terminal command:', error.message);
			return;
		}

	}
	async setDynamic(){
		
		try {	
			const { stdout, stderr } = await exec1('nmcli con mod eth ipv4.method auto');
		} catch (error) {
			// this.log('error', 'Failed to run terminal command:', error.message);
			return;
		}
		

	}

	async updateNetwork(type, ipsetting)
	{
		let ip1 = this.getVariableValue('ip_1');
		let ip2 = this.getVariableValue('ip_2');
		let ip3 = this.getVariableValue('ip_3');
		let ip4 = this.getVariableValue('ip_4');

		let setting = ip1 + '.' +ip2+'.'+ip3+'.'+ip4;
		console.log('the ip to set is ', setting);
		if(ipsetting == 'ip4')
		{
			setting = setting + '/24';
		}
		try {
			const { stdout, stderr } = await exec1('nmcli con mod eth ' + type + ' ' + setting);
			// console.log('the response is ', stdout);
		} catch (error) {
			// this.log('error', 'Failed to run terminal command:', error.message);
			return;
		}
	}

	async runTerminalCmd(searchterm, setting){
		//console.log('in runcmd');
		try {
			const { stdout, stderr } = await exec1('nmcli d show eth0 | grep -i '+ searchterm);
			let value = stdout.split(':')[1].trim();
			if(setting == 'ip4')
		{
			value = value.split('/')[0];
		}
		 
			this.setVariableValues({[setting]:value});
		} catch (error) {
			// this.log('error', 'Failed to run terminal command:', error.message);
			return;
		}
		
	}

	// setVariableValues(variables) {
	// 	super.setVariableValues(variables)
	// 	// this.checkFeedbacks('scenePreview') // Ensure feedbacks are checked after setting variable values
	// }
	
	updateDSKDisplay(input) {
		// Get the current dsk_display variable value if no input provided
		const dskDisplayInput = input || this.getVariableValue('dsk_display') || '';
		
		if (!dskDisplayInput) {
			this.setVariableValues({ dsk_display: '' });
			return;
		}
		
		// Parse format: "tab:item" (e.g., "1:DSK 1")
		const parts = dskDisplayInput.split(':');
		if (parts.length !== 2) {
			this.setVariableValues({ dsk_display: 'Invalid format' });
			return;
		}
		
		const tabNumber = parseInt(parts[0]);
		const itemName = parts[1].trim();
		
		if (isNaN(tabNumber) || tabNumber < 1) {
			this.setVariableValues({ dsk_display: 'Invalid tab number' });
			return;
		}
		
		const tabIndex = tabNumber - 1; // Convert to 0-based
		
		// Check if we have state for this DSK item
		const isEnabled = this.dskStates?.[tabIndex]?.[itemName];
		if (isEnabled === undefined) {
			this.setVariableValues({ dsk_display: `${itemName}: Unknown` });
			return;
		}
		
		// Set the display value
		const status = isEnabled ? 'ON' : 'OFF';
		this.setVariableValues({ dsk_display: `${itemName}: ${status}` });
		
		console.log(`DSK Display updated: ${itemName}: ${status}`);
	}

	validateMediaButtonText(media) {
		if (!media || !this.mediaSources[media]) {
			return
		}
		
		const mediaState = this.mediaSources[media].mediaState
		const currentButtonText = this.getVariableValue('current_media_button_text') || 'Play'
		
		let correctButtonText = 'Play' // Default
		if (mediaState === 'CRE8_MEDIA_STATE_PLAYING') {
			correctButtonText = 'Pause' // Show "Pause" when playing
		} else {
			correctButtonText = 'Play' // Show "Play" when paused/stopped
		}
		
		// Only update if out of sync
		if (currentButtonText !== correctButtonText) {
			console.log(`Media button text out of sync. Correcting from "${currentButtonText}" to "${correctButtonText}"`)
			this.setVariableValues({
				current_media_button_text: correctButtonText
			})
		}
	}
}
runEntrypoint(CRE8Instance, UpgradeScripts)

