import {
	BLE_SERVICE_UUID,
	BLE_CHAR_FLASHING_UUID,
	BLE_CHAR_SERIAL_UUID,
	CMD_INIT, CMD_DATA, CMD_END, CMD_ACK, CMD_ERR, CMD_SET_BAUD, CMD_RESET,
	CHUNK_SIZE, BLE_TIMEOUT_MS, END_TIMEOUT_MS, END_ACK_INDEX, INIT_ACK_INDEX, MAX_RETRIES,
	type HardwareDeployer,
	type DeployProgress, type BLEACKResponse
} from '$types/deployer';

let _ackNotificationCount = 0;
function logAck(tag: string, msg: string) {
	console.log(`[BLE-ACK#${++_ackNotificationCount}] ${tag}: ${msg}`);
}

export class BLEHardwareDeployer implements HardwareDeployer {
	private server: BluetoothRemoteGATTServer | null = null;
	private service: BluetoothRemoteGATTService | null = null;
	private flashingChar: BluetoothRemoteGATTCharacteristic | null = null;
	private serialChar: BluetoothRemoteGATTCharacteristic | null = null;
	private device: BluetoothDevice | null = null;

	private onSerialData: ((text: string) => void) | null = null;

	/** Map of expected ACK index → resolver pair. Supports concurrent waits. */
	private ackResolvers = new Map<number, {
		resolve: (value: BLEACKResponse) => void;
		reject: (reason: Error) => void;
		timer: ReturnType<typeof setTimeout>;
	}>();

	/** ACKs that arrived before waitForAck was called, keyed by index. */
	private pendingAcks = new Map<number, BLEACKResponse>();

	private _onDisconnected: (() => void) | null = null;

	/** Bound handler refs so we can removeEventListener on cleanup/re-pair. */
	private _onDeviceDisconnect: (() => void) | null = null;
	private _onFlashingNotification: ((event: Event) => void) | null = null;
	private _onSerialNotification: ((event: Event) => void) | null = null;

	checkSupport(): boolean {
		return typeof navigator !== 'undefined' && 'bluetooth' in navigator;
	}

	get isConnected(): boolean {
		return this.server?.connected ?? false;
	}

	get deviceName(): string | null {
		return this.device?.name ?? null;
	}

	private withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
		return Promise.race([
			promise,
			new Promise<T>((_, reject) => {
				setTimeout(() => reject(new Error(`Timeout: ${label} (${ms}ms)`)), ms);
			})
		]);
	}

	async pair(): Promise<void> {
		if (!this.checkSupport()) {
			throw new Error('Web Bluetooth tidak didukung di browser ini. Gunakan Chrome Android.');
		}

		// Clean up any previous session's listeners before re-pairing
		this.cleanup();

		console.log('[BLE] step 1: requestDevice...');
		this.device = await navigator.bluetooth.requestDevice({
			filters: [{ namePrefix: 'Velxio' }],
			optionalServices: [BLE_SERVICE_UUID]
		});
		console.log('[BLE] step 1: device selected:', this.device.name);

		this._onDeviceDisconnect = () => {
			console.log('[BLE] gattserverdisconnected event');
			this._onDisconnected?.();
			this.cleanup();
		};
		this.device.addEventListener('gattserverdisconnected', this._onDeviceDisconnect);

		console.log('[BLE] step 2: gatt.connect...');
		this.server = await this.withTimeout(
			this.device.gatt!.connect(),
			10000,
			'gatt.connect'
		);
		console.log('[BLE] step 2: connected, mtu=' + this.server.mtu);

		console.log('[BLE] step 3: getPrimaryService...');
		this.service = await this.withTimeout(
			this.server.getPrimaryService(BLE_SERVICE_UUID),
			5000,
			'getPrimaryService'
		);
		console.log('[BLE] step 3: service found');

		console.log('[BLE] step 4: getCharacteristic flashing...');
		this.flashingChar = await this.withTimeout(
			this.service.getCharacteristic(BLE_CHAR_FLASHING_UUID),
			5000,
			'getCharacteristic(flashing)'
		);
		console.log('[BLE] step 4: flashing char found');

		console.log('[BLE] step 5: getCharacteristic serial...');
		this.serialChar = await this.withTimeout(
			this.service.getCharacteristic(BLE_CHAR_SERIAL_UUID),
			5000,
			'getCharacteristic(serial)'
		);
		console.log('[BLE] step 5: serial char found');

		console.log('[BLE] step 6: requestMTU(255)...');
		try {
			await this.withTimeout(
				this.server.requestMTU(255),
				3000,
				'requestMTU'
			);
			console.log('[BLE] step 6: MTU negotiated, now ' + this.server.mtu);
		} catch (e) {
			console.log('[BLE] step 6: MTU fallback, mtu=' + this.server.mtu, e);
		}

		console.log('[BLE] step 7: startNotifications...');
		await this.withTimeout(
			this.flashingChar.startNotifications(),
			5000,
			'startNotifications'
		);
		console.log('[BLE] step 7: notifications subscribed');

		this._onFlashingNotification = (event: Event) => {
			const target = event.target as BluetoothRemoteGATTCharacteristic;
			const dv = target.value!;
			const value = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
			this.handleFlashingNotification(value);
		};
		this.flashingChar.addEventListener('characteristicvaluechanged', this._onFlashingNotification);
		console.log('[BLE] pair() completed successfully');
	}

	private handleFlashingNotification(value: Uint8Array) {
		const cmd = value[0];
		const index = value[1] | (value[2] << 8);
		const cmdName = cmd === CMD_ACK ? 'ACK' : cmd === CMD_ERR ? 'ERR' : 'UNKNOWN';
		logAck(cmdName, `index=${index} len=${value.length}`);

		if (cmd === CMD_ACK) {
			const ack: BLEACKResponse = { command: cmd, index, status: 'OK' };

			// Check if someone is waiting for this exact index
			const pending = this.ackResolvers.get(index);
			if (pending) {
				logAck(cmdName, `Found resolver for index=${index} — resolving`);
				this.ackResolvers.delete(index);
				clearTimeout(pending.timer);
				pending.resolve(ack);
			} else if ((this.ackResolvers.has(0xFFFF) || this.ackResolvers.has(INIT_ACK_INDEX)) && index !== 0xFFFF && index !== INIT_ACK_INDEX) {
				// No one waiting for this specific index right now,
				// but someone IS waiting for END (0xFFFF) or INIT
				// (INIT_ACK_INDEX) — don't let a stale chunk ACK
				// resolve them. Only END/INIT sentinel ACKs are
				// relevant while these waits are active.
				logAck(cmdName, `index=${index} no resolver, waitForAck(0xFFFF/0xFFFE) active — dropping stale ACK`);
			} else {
				// Cache for a future waitForAck call
				logAck(cmdName, `index=${index} no resolver — caching as pendingAck`);
				this.pendingAcks.set(index, ack);
			}
		} else if (cmd === CMD_ERR) {
			const msgLen = value[3];
			const msg = msgLen > 0
				? new TextDecoder().decode(value.slice(4, 4 + msgLen))
				: 'Unknown error';
			logAck(cmdName, `index=${index} ERR: ${msg}`);

			const pending = this.ackResolvers.get(index);
			if (pending) {
				logAck(cmdName, `Found rejecter for index=${index} — rejecting`);
				this.ackResolvers.delete(index);
				clearTimeout(pending.timer);
				pending.reject(new Error(`ERR: ${msg}`));
			} else {
				logAck(cmdName, `index=${index} no rejecter — ERR lost: ${msg}`);
			}
		}
	}

	async deployHex(hexContent: string, onProgress: (p: DeployProgress) => void): Promise<void> {
		if (!this.flashingChar) throw new Error('Belum terhubung ke perangkat');

		/* Stop serial monitor before re-deploy — reduces BLE traffic and
		 * frees the firmware's serial bridge (which consumes mbuf pool
		 * that the INIT ACK needs). */
		this.stopSerialMonitor();

		/* Ensure flashing char CCCD is subscribed before sending INIT.
		 * The END flow's startNotifications is fire-and-forget and may
		 * not have completed; if CCCD was left at 0 (possible on some
		 * Android BLE stacks), the INIT ACK notification is silently
		 * dropped, causing the re-deploy to hang. */
		try {
			await this.withTimeout(
				this.flashingChar.startNotifications(),
				3000,
				'startNotifications(re-subscribe)'
			);
			console.log('[BLE] D: flashing char re-subscribed (CCCD=1)');
		} catch (e) {
			console.log('[BLE] D: startNotifications failed (non-critical, read-poll fallback)', e);
		}

		const binaryData = this.hexToBinary(hexContent);
		const totalChunks = Math.ceil(binaryData.length / CHUNK_SIZE);
		const totalCRC = this.crc32(binaryData);

		console.log(`[BLE] deployHex: ${binaryData.length} bytes, ${totalChunks} chunks, CRC=0x${totalCRC.toString(16)}`);

		/* Clear stale ACKs from a previous deploy (prevents cross-deploy
		 * poisoning where a late ACK from deploy N resolves deploy N+1). */
		this.pendingAcks.clear();

		onProgress({ state: 'transferring', message: 'Mengirim INIT...', totalChunks, completedChunks: 0 });
		console.log('[BLE] D: sending INIT...');
		await this.sendCommand(CMD_INIT, INIT_ACK_INDEX, this.uint32ToBytes(totalCRC));
		console.log('[BLE] D: INIT done');

		for (let i = 0; i < totalChunks; i++) {
			const start = i * CHUNK_SIZE;
			const end = Math.min(start + CHUNK_SIZE, binaryData.length);
			const chunk = binaryData.slice(start, end);
			const chunkCRC = this.crc32(chunk);

			await this.sendCommandWithRetry(CMD_DATA, i, chunk, chunkCRC);

			onProgress({
				state: 'transferring',
				message: `Mengirim chunk ${i + 1}/${totalChunks}`,
				totalChunks,
				completedChunks: i + 1
			});
		}
		console.log('[BLE] D: all DATA chunks done');

		onProgress({ state: 'flashing', message: 'Verifikasi & flashing...', totalChunks, completedChunks: totalChunks });
		console.log('[BLE] D: END starting (30s timeout)...');
		const startTime = Date.now();
		try {
			await this.sendCommand(CMD_END, END_ACK_INDEX, this.uint32ToBytes(totalCRC), END_TIMEOUT_MS);
			const elapsed = Date.now() - startTime;
			console.log(`[BLE] D: END ACK received after ${elapsed}ms, flash success!`);
		} catch (err) {
			const elapsed = Date.now() - startTime;
			console.log(`[BLE] D: END FAILED after ${elapsed}ms:`, err);
			throw err;
		}

		onProgress({ state: 'serial_bridge', message: 'Flash berhasil! Membuka Serial Monitor...', totalChunks, completedChunks: totalChunks });
	}

	private async sendCommand(cmd: number, index: number, data: Uint8Array, timeoutMs?: number): Promise<void> {
		const cmdName = cmd === CMD_INIT ? 'INIT' : cmd === CMD_DATA ? 'DATA' : cmd === CMD_END ? 'END' : `0x${cmd.toString(16)}`;
		const payload = new Uint8Array(4 + data.length + 4);
		payload[0] = cmd;
		payload[1] = index & 0xFF;
		payload[2] = (index >> 8) & 0xFF;
		payload[3] = data.length;
		payload.set(data, 4);

		const crc = this.crc32(data);
		payload.set([
			crc & 0xFF, (crc >> 8) & 0xFF, (crc >> 16) & 0xFF, (crc >> 24) & 0xFF
		], 4 + data.length);

		const ackTimeout = timeoutMs ?? BLE_TIMEOUT_MS;
		console.log(`[BLE-CMD] ${cmdName} idx=${index}: waitForAck timeout=${ackTimeout}ms start`);
		const ackPromise = this.waitForAck(index, ackTimeout);
		try {
			console.log(`[BLE-CMD] ${cmdName} idx=${index}: writing payload (${payload.length} bytes)...`);
			await this.withTimeout(
				this.flashingChar!.writeValueWithResponse(payload),
				3000,
				'writeValueWithResponse'
			);
			console.log(`[BLE-CMD] ${cmdName} idx=${index}: write done, now waiting for ACK...`);
		} catch (e) {
			console.log(`[BLE-CMD] ${cmdName} idx=${index}: write failed, still waiting for ACK`, e);
		}

		/* END (flash ~8s): Android BLE stack (Qualcomm) drops
		 * characteristicvaluechanged events after ~8s idle, so ACK
		 * notification is unreliable.  Use readValue() to poll the
		 * firmware's state machine instead — GATT Read is client-
		 * initiated and NOT affected by the notify idle-drop bug.
		 *
		 * CCCD refresh is fire-and-forget as a belt-and-suspenders:
		 * it didn't solve the issue on Qualcomm, but may help on
		 * other devices. */
		if (cmd === CMD_END) {
			/* Re-subscribe flashing char (fire-and-forget). Do NOT call
			 * stopNotifications() — that writes CCCD=0 and if the subsequent
			 * startNotifications fails (race with readValue poll), the
			 * flashing char is left unsubscribed, breaking re-deploy INIT ACK. */
			this.flashingChar!.startNotifications().then(
				() => console.log('[BLE-CMD] END: CCCD refreshed (fire-and-forget)'),
				(e: unknown) => console.log('[BLE-CMD] END: CCCD refresh failed (non-critical)', e)
			);

			/* Read-based state poll — the RELIABLE path.
			 * State values from state_machine.h:
			 *   0=IDLE, 1=RECEIVING, 2=VERIFYING, 3=FLASHING (keep polling),
			 *   4=SERIAL_BRIDGE (success), 5=ERROR_TARGET, 6=ERROR_CHECKSUM */
			const STATE_SERIAL_BRIDGE = 4;
			const STATE_ERROR_TARGET = 5;
			const STATE_ERROR_CHECKSUM = 6;

			const readPoll = (async () => {
				const deadline = Date.now() + ackTimeout;
				let firstPoll = true;
				while (Date.now() < deadline) {
					try {
						const dv = await this.withTimeout(
							this.flashingChar!.readValue(),
							3000,
							'readValue(state)'
						);
						const state = dv.getUint8(0);
						console.log(`[BLE-CMD] END: readValue state=${state}`);

						if (state === STATE_SERIAL_BRIDGE) {
							console.log('[BLE-CMD] END: state=SERIAL_BRIDGE — flash success via read poll');
							return 'ok' as const;
						}
						if (state === STATE_ERROR_TARGET || state === STATE_ERROR_CHECKSUM) {
							console.log(`[BLE-CMD] END: state=ERROR(${state}) — flash failed via read poll`);
							throw new Error('Flash gagal di sisi firmware (state=' + state + ')');
						}
						/* state=FLASHING or VERIFYING — keep polling */
					} catch (e) {
						/* readValue failed (GATT error or timeout) — keep
						 * retrying unless the connection itself is dead. */
						if (!this.isConnected) {
							throw new Error('Koneksi BLE terputus saat menunggu flash');
						}
						console.log('[BLE-CMD] END: readValue retry...', e);
					}
					/* First poll at 200ms (fast check if flash already done),
					 * then every 500ms. ~16 polls during 8s flash. */
					await new Promise<void>(r => setTimeout(r, firstPoll ? 200 : 500));
					firstPoll = false;
				}
				console.log('[BLE-CMD] END: readPoll deadline passed (30s)');
				return 'timeout' as const;
			})();

			/* Race: ACK notify (fast path — works on some devices) vs
			 * readValue poll (reliable path — works everywhere). */
			const ackWithLabel = ackPromise.then(() => 'ack' as const);
			const result = await Promise.race([ackWithLabel, readPoll]);
			console.log(`[BLE-CMD] END idx=${index}: resolved via ${result}`);

			/* Clean up the ackPromise resolver if readPoll won —
			 * prevents a dangling 30s timer. */
			if (result !== 'ack') {
				const entry = this.ackResolvers.get(index);
				if (entry) {
					clearTimeout(entry.timer);
					this.ackResolvers.delete(index);
					console.log(`[BLE-CMD] END: cleaned up ackPromise resolver (won via ${result})`);
				}
			}

			if (result === 'timeout') {
				throw new Error(`Timeout menunggu ACK untuk chunk ${index}`);
			}
			return;
		}

		/* INIT: Firmware sets state to RECEIVING(1) on success — poll for it.
		 * DATA does NOT go through sendCommand (uses sendCommandWithRetry
		 * with direct write+waitForAck — no readPoll). */
		if (cmd === CMD_INIT) {
			const readPoll = (async () => {
				const deadline = Date.now() + ackTimeout;
				while (Date.now() < deadline) {
					try {
						const dv = await this.withTimeout(
							this.flashingChar!.readValue(),
							3000,
							'readValue(state)'
						);
						const state = dv.getUint8(0);
						console.log(`[BLE-CMD] INIT: readValue state=${state}`);

						if (state >= 5) {
							throw new Error(`Firmware error (state=${state})`);
						}
						/* Only RECEIVING(1) means INIT was processed.
						 * Don't accept FLASHING(3) or SERIAL_BRIDGE(4) —
						 * those are stale states from a previous deploy. */
						if (state === 1) {
							console.log(`[BLE-CMD] INIT: state=1 (RECEIVING) — INIT confirmed via read poll`);
							return 'ok' as const;
						}
					} catch (e) {
						if (!this.isConnected) {
							throw new Error('Koneksi BLE terputus');
						}
						console.log(`[BLE-CMD] INIT: readValue retry...`, e);
					}
					await new Promise<void>(r => setTimeout(r, 200));
				}
				console.log(`[BLE-CMD] INIT: readPoll deadline passed (${ackTimeout}ms)`);
				return 'timeout' as const;
			})();

			const ackWithLabel = ackPromise.then(() => 'ack' as const);
			const result = await Promise.race([ackWithLabel, readPoll]);
			console.log(`[BLE-CMD] INIT idx=${index}: resolved via ${result}`);

			if (result !== 'ack') {
				const entry = this.ackResolvers.get(index);
				if (entry) {
					clearTimeout(entry.timer);
					this.ackResolvers.delete(index);
				}
			}
			if (result === 'timeout') {
				throw new Error(`Timeout menunggu ACK untuk chunk ${index}`);
			}
			return;
		}
	}

	private async sendCommandWithRetry(cmd: number, index: number, data: Uint8Array, chunkCRC: number): Promise<void> {
		/* Direct write + waitForAck (no readPoll) — reverts cad90eb.
		 * writeValueWithResponse already confirms delivery via GATT
		 * response; the ACK notify confirms per-chunk CRC. During
		 * active data transfer there is no idle gap, so ACK notifies
		 * are NOT subject to the Qualcomm idle-drop bug */
		const payload = new Uint8Array(4 + data.length + 4);
		payload[0] = cmd;
		payload[1] = index & 0xFF;
		payload[2] = (index >> 8) & 0xFF;
		payload[3] = data.length;
		payload.set(data, 4);
		payload.set([
			chunkCRC & 0xFF, (chunkCRC >> 8) & 0xFF, (chunkCRC >> 16) & 0xFF, (chunkCRC >> 24) & 0xFF
		], 4 + data.length);

		for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
			try {
				const ackPromise = this.waitForAck(index);
				await this.withTimeout(
					this.flashingChar!.writeValueWithResponse(payload),
					3000,
					'writeValueWithResponse'
				);
				await ackPromise;
				return;
			} catch (err) {
				if (attempt === MAX_RETRIES - 1) throw err;
				console.log(`[BLE-CMD] DATA idx=${index}: retry ${attempt + 1}/${MAX_RETRIES}`, err);
				await new Promise(r => setTimeout(r, 500));
			}
		}
	}

	private waitForAck(expectedIndex: number, timeoutMs: number = BLE_TIMEOUT_MS): Promise<BLEACKResponse> {
		return new Promise((resolve, reject) => {
			// Check for a cached ACK that arrived before we started waiting
			const cached = this.pendingAcks.get(expectedIndex);
			if (cached) {
				this.pendingAcks.delete(expectedIndex);
				logAck('WAIT', `pendingAcks hit for index=${expectedIndex} actual=${cached.index} — resolving immediately`);
				resolve(cached);
				return;
			}

			logAck('WAIT', `Registering resolver for index=${expectedIndex} timeout=${timeoutMs}ms`);

			const timer = setTimeout(() => {
				const entry = this.ackResolvers.get(expectedIndex);
				if (entry) {
					this.ackResolvers.delete(expectedIndex);
					logAck('WAIT', `TIMEOUT after ${timeoutMs}ms for index=${expectedIndex} — rejecting`);
					reject(new Error(`Timeout menunggu ACK untuk chunk ${expectedIndex}`));
				} else {
					logAck('WAIT', `Timeout fired but resolver already consumed for index=${expectedIndex} — ignoring`);
				}
			}, timeoutMs);

			this.ackResolvers.set(expectedIndex, { resolve, reject, timer });
		});
	}

	async startSerialMonitor(onData: (text: string) => void): Promise<void> {
		if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');

		this.onSerialData = onData;

		console.log('[BLE-SERIAL] startSerialMonitor: requesting notifications...');
		await this.serialChar.startNotifications();
		console.log('[BLE-SERIAL] startNotifications OK');
		this._onSerialNotification = (event: Event) => {
			const target = event.target as BluetoothRemoteGATTCharacteristic;
			const dv = target.value!;
			const value = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
			const text = new TextDecoder().decode(value);
			console.log(`[BLE-SERIAL] RX: ${value.length}B "${text.substring(0, 60)}"`);
			this.onSerialData?.(text);
		};
		this.serialChar.addEventListener('characteristicvaluechanged', this._onSerialNotification);
		console.log('[BLE-SERIAL] listener attached');
	}

	async sendSerialInput(text: string): Promise<void> {
		if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');
		console.log(`[BLE-SERIAL] TX: "${text.substring(0, 60)}"`);
		const encoder = new TextEncoder();
		await this.serialChar.writeValueWithoutResponse(encoder.encode(text));
	}

	async setBaudRate(baud: number): Promise<void> {
		if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');
		console.log(`[BLE-SERIAL] setBaudRate: ${baud}`);
		const payload = new Uint8Array(5);
		payload[0] = CMD_SET_BAUD;
		payload[1] = baud & 0xFF;
		payload[2] = (baud >> 8) & 0xFF;
		payload[3] = (baud >> 16) & 0xFF;
		payload[4] = (baud >> 24) & 0xFF;
		await this.serialChar.writeValueWithoutResponse(payload);
	}

	async resetArduino(): Promise<void> {
		if (!this.serialChar) throw new Error('Belum terhubung ke perangkat');

		console.log('[BLE-SERIAL] resetArduino: sending DTR pulse command...');

		const payload = new Uint8Array(1);
		payload[0] = CMD_RESET;

		await this.serialChar.writeValueWithoutResponse(payload);

		// Wait for firmware to complete DTR pulse (250ms delay via firmware DTR pulse timing)
		await new Promise(resolve => setTimeout(resolve, 250));

		console.log('[BLE-SERIAL] resetArduino: DTR pulse complete');
	}

	stopSerialMonitor(): void {
		console.log('[BLE-SERIAL] stopSerialMonitor');
		if (this.serialChar && this._onSerialNotification) {
			this.serialChar.removeEventListener('characteristicvaluechanged', this._onSerialNotification);
			this.serialChar.stopNotifications();
		}
		this._onSerialNotification = null;
		this.onSerialData = null;
	}

	disconnect(): void {
		this.stopSerialMonitor();
		if (this.flashingChar) {
			this.flashingChar.stopNotifications();
		}
		this.server?.disconnect();
		this.cleanup();
	}

	onDisconnected(callback: () => void): void {
		this._onDisconnected = callback;
	}

	private cleanup(): void {
		// Remove device-level event listeners
		if (this.device && this._onDeviceDisconnect) {
			this.device.removeEventListener('gattserverdisconnected', this._onDeviceDisconnect);
		}
		this._onDeviceDisconnect = null;

		// Remove flashing notification listener
		if (this.flashingChar && this._onFlashingNotification) {
			this.flashingChar.removeEventListener('characteristicvaluechanged', this._onFlashingNotification);
		}
		this._onFlashingNotification = null;

		// Remove serial notification listener
		if (this.serialChar && this._onSerialNotification) {
			this.serialChar.removeEventListener('characteristicvaluechanged', this._onSerialNotification);
		}
		this._onSerialNotification = null;

		this.server = null;
		this.service = null;
		this.flashingChar = null;
		this.serialChar = null;
		this.device = null;
		// Clear all pending ACK resolvers
		for (const [idx, entry] of this.ackResolvers) {
			clearTimeout(entry.timer);
		}
		this.ackResolvers.clear();
		this.pendingAcks.clear();
		this.onSerialData = null;
	}

	private hexToBinary(hexContent: string): Uint8Array {
		const bytes: number[] = [];
		let extendedAddress = 0;

		for (const line of hexContent.split('\n')) {
			const trimmed = line.trim();
			if (!trimmed || trimmed[0] !== ':') continue;

			const byteCount = parseInt(trimmed.substring(1, 3), 16);
			const address = parseInt(trimmed.substring(3, 7), 16) + extendedAddress;
			const recordType = parseInt(trimmed.substring(7, 9), 16);

			if (recordType === 0x00) {
				for (let i = 0; i < byteCount; i++) {
					const byteOffset = 9 + i * 2;
					if (byteOffset + 2 <= trimmed.length) {
						const byteVal = parseInt(trimmed.substring(byteOffset, byteOffset + 2), 16);
						bytes[address + i] = byteVal;
					}
				}
			} else if (recordType === 0x04) {
				extendedAddress = parseInt(trimmed.substring(9, 13), 16) << 16;
			} else if (recordType === 0x01) {
				break;
			}
		}

		const result = new Uint8Array(bytes.length);
		for (let i = 0; i < bytes.length; i++) {
			result[i] = bytes[i] ?? 0;
		}
		return result;
	}

	private crc32Table: Uint32Array | null = null;

	private crc32(data: Uint8Array): number {
		if (!this.crc32Table) {
			this.crc32Table = new Uint32Array(256);
			for (let i = 0; i < 256; i++) {
				let c = i;
				for (let j = 0; j < 8; j++) {
					c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
				}
				this.crc32Table[i] = c;
			}
		}
		let crc = 0xFFFFFFFF;
		for (let i = 0; i < data.length; i++) {
			crc = this.crc32Table[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
		}
		return (crc ^ 0xFFFFFFFF) >>> 0;
	}

	private uint32ToBytes(value: number): Uint8Array {
		return new Uint8Array([
			value & 0xFF,
			(value >> 8) & 0xFF,
			(value >> 16) & 0xFF,
			(value >> 24) & 0xFF
		]);
	}
}
