const {
	InstanceBase,
	runEntrypoint,
	InstanceStatus,
} = require('@companion-module/base')

const UpgradeScripts = require('./upgrades')
const UpdateActions = require('./actions')
const UpdateFeedbacks = require('./feedbacks')
const UpdateVariableDefinitions = require('./variables')

const { SerialPort } = require('serialport')

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

		this.config = {}
		this.port = null
		this.portPath = null
		this.pendingCommand = null

		// Internal state for feedbacks (true = pressed)
		this.buttonStates = {
			1: false,
			2: false,
			3: false,
			4: false,
			5: false,
			6: false,
		}

		// Cached serial port choices for dropdown UI
		this.serialChoices = []

		// GO BOX 2 USB identifiers (hex, from your actual device)
		this.goBoxVendorId = '239a'
		this.goBoxProductId = 'cafe'

		this.buttonNames = {
			1: 'GO',
			2: 'STOP',
			3: 'LAST',
			4: 'NEXT',
			5: 'LOAD',
			6: 'PAUSE',
		}
	}

	// ---------------------------------------------------------------------------
	// Lifecycle
	// ---------------------------------------------------------------------------

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

		await this.refreshSerialChoices()

		if (!this.config.path) {
			const autoPath = this.autoSelectPortPath()

			if (autoPath) {
				this.log('info', `Auto-selected GO BOX 2: ${autoPath}`)
				this.config.path = autoPath

				try {
					this.saveConfig(this.config)
				} catch (_) {
					if (typeof this.updateConfig === 'function') {
						this.updateConfig(this.config)
					}
				}
			}
		}

		await this.initSerial()

		this.updateActions()
		this.updateFeedbacks()
		this.updateVariableDefinitions()

		this.updateStatus(InstanceStatus.Ok)
	}

	async destroy() {
		this.log('debug', 'destroy')

		if (this.port) {
			try {
				await this.closeSerial()
			} catch (e) {
				this.log('debug', `Error closing serial on destroy: ${e}`)
			}
		}
	}

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

		await this.refreshSerialChoices()
		await this.initSerial()
	}

	// ---------------------------------------------------------------------------
	// Config Fields (must be synchronous)
	// ---------------------------------------------------------------------------

	getConfigFields() {
		const portChoices = Array.isArray(this.serialChoices) ? this.serialChoices : []

		return [
			{
				type: 'static-text',
				id: 'info',
				label: 'Info',
				width: 12,
				value:
					'Connect GO BOX 2 in Companion mode. If detected, it appears in the list below.',
			},
			{
				type: 'dropdown',
				id: 'path',
				label: 'Serial Device',
				width: 12,
				choices: portChoices,
				default: portChoices.length === 1 ? portChoices[0].id : (this.config.path || ''),
				allowCustom: true,
			},
			{
				type: 'number',
				id: 'baudRate',
				label: 'Baud Rate',
				width: 4,
				default: 115200,
				min: 1200,
				max: 1000000,
			},
		]
	}

	// ---------------------------------------------------------------------------
	// Serial Auto-Detection
	// ---------------------------------------------------------------------------

	async refreshSerialChoices() {
		let ports = []
	
		try {
			ports = await SerialPort.list()
		} catch (err) {
			this.log('debug', `SerialPort.list failed: ${err}`)
			ports = []
		}
	
		const choices = []
	
		for (const p of ports || []) {
			const path = p.path || ''
			if (!path) continue
	
			const vid = (p.vendorId || '').toString().toLowerCase()
			const pid = (p.productId || '').toString().toLowerCase()
			const manufacturer = p.manufacturer || ''
	
			// Build simplified label: path + optional manufacturer
			let label = path
			if (manufacturer) {
				label += ` — ${manufacturer}`
			}
	
			choices.push({
				id: path,
				label,
				_vid: vid,   // keep internally for auto-select
				_pid: pid,
			})
		}
	
		this.serialChoices = choices
	}
	

	autoSelectPortPath() {
		const choices = this.serialChoices

		// Prefer exact GO BOX 2 match
		const match = choices.find(
			(c) => c._vid === this.goBoxVendorId && c._pid === this.goBoxProductId
		)
		if (match) return match.id

		// If only one device exists, pick it
		if (choices.length === 1) {
			return choices[0].id
		}

		return ''
	}

	// ---------------------------------------------------------------------------
	// Serial Handling
	// ---------------------------------------------------------------------------

	async initSerial() {
		const path = this.config.path || ''
		const baudRate = Number(this.config.baudRate) || 115200

		if (this.port) {
			if (this.portPath !== path || this.port.baudRate !== baudRate) {
				await this.closeSerial()
			} else if (this.port.isOpen) {
				return
			}
		}

		if (!path) {
			this.updateStatus(InstanceStatus.Disconnected, 'No serial path selected')
			return
		}

		try {
			this.log('info', `Opening serial ${path} @ ${baudRate}`)
			this.portPath = path

			this.port = new SerialPort({
				path,
				baudRate,
				autoOpen: true,
			})

			this.port.on('open', () => {
				this.log('info', `Serial port open: ${path}`)
				this.updateStatus(InstanceStatus.Ok)
			})

			this.port.on('error', (err) => {
				this.log('error', `Serial error ${path}: ${err.message}`)
				this.updateStatus(InstanceStatus.Error, err.message)
			})

			this.port.on('close', () => {
				this.log('info', `Serial closed: ${path}`)
				this.updateStatus(InstanceStatus.Disconnected)
			})

			this.port.on('data', (data) => {
				this.handleSerialData(data)
			})
		} catch (err) {
			this.log('error', `Failed to open serial ${path}: ${err.message}`)
			this.updateStatus(InstanceStatus.Error, err.message)
			this.port = null
		}
	}

	async closeSerial() {
		if (!this.port) return

		const closingPort = this.port
		this.port = null

		return new Promise((resolve) => {
			if (!closingPort.isOpen) return resolve()

			closingPort.close((err) => {
				if (err) this.log('debug', `Error closing serial: ${err.message}`)
				resolve()
			})
		})
	}

	// ---------------------------------------------------------------------------
	// CDC Input Parsing
	// ---------------------------------------------------------------------------

	handleSerialData(data) {
		if (!data || !data.length) return

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

			if (this.pendingCommand === 'D' || this.pendingCommand === 'U') {
				const id = b
				const pressed = this.pendingCommand === 'D'
				this.pendingCommand = null

				this.handleButtonEvent(id, pressed)
				continue
			}

			if (b === 0x44) {
				this.pendingCommand = 'D'
			} else if (b === 0x55) {
				this.pendingCommand = 'U'
			}
		}
	}

	handleButtonEvent(id, pressed) {
		const name = this.buttonNames[id] || `Button ${id}`
		const stateStr = pressed ? 'down' : 'up'

		this.log('debug', `Button ${id} (${name}) ${stateStr}`)

		// Update variables
		this.setVariableValues({
			last_button: `${id} / ${name}`,
			last_event: stateStr,
			last_button_id: id,
			[`button_${id}_state`]: pressed ? 'pressed' : 'released',
		})

		// Update internal feedback tracking
		this.buttonStates[id] = pressed

		// Re-evaluate all feedbacks
		if (this.checkFeedbacks) this.checkFeedbacks()
	}

	// ---------------------------------------------------------------------------
	// Boilerplate passthroughs
	// ---------------------------------------------------------------------------

	updateActions() {
		UpdateActions(this)
	}

	updateFeedbacks() {
		UpdateFeedbacks(this)
	}

	updateVariableDefinitions() {
		UpdateVariableDefinitions(this)
	}
}

runEntrypoint(UsbGoBox2CdcInstance, UpgradeScripts)
