import type { CorruptedFrame, Frame, ZnifferOptions } from 'zwave-js'
import {
	CommandClass,
	isEncapsulatingCommandClass,
	isMultiEncapsulatingCommandClass,
	Zniffer,
} from 'zwave-js'
import { TypedEventEmitter } from './EventEmitter.ts'
import { module } from './logger.ts'
import type { Server as SocketServer } from 'socket.io'
import { socketEvents } from './SocketEvents.ts'
import type { ZwaveConfig } from './ZwaveClient.ts'
import { logsDir, storeDir } from '../config/app.ts'
import { buffer2hex, joinPath, parseSecurityKeys } from './utils.ts'
import { isDocker } from './utils.ts'
import { basename } from 'node:path'
import { readFile } from 'node:fs/promises'
import tripleBeam from 'triple-beam'

const loglevels = tripleBeam.configs.npm.levels

export type ZnifferConfig = Pick<
	ZwaveConfig,
	| 'securityKeys'
	| 'securityKeysLongRange'
	| 'maxFiles'
	| 'logEnabled'
	| 'logToFile'
	| 'logLevel'
	| 'nodeFilter'
> & {
	port: string
	enabled: boolean
	convertRSSI?: boolean
	defaultFrequency?: number
}

export type ZnifferManagerEventCallbacks = Record<string, never>

const logger = module('ZnifferManager')

const ZNIFFER_LOG_FILE = joinPath(logsDir, 'zniffer_%DATE%.log')
const ZNIFFER_CAPTURE_FILE = joinPath(storeDir, 'zniffer_capture_%DATE%.zlf')

export type SocketFrame = (Frame | CorruptedFrame) & {
	payload: string
	parsedPayload?: Record<string, any>
	corrupted: boolean
	timestamp: number
	raw: string
}

export interface FrameCCLogEntry {
	tags: string[]
	message?: {
		encapsulated?: FrameCCLogEntry[]
		[key: string]: string | number | boolean | FrameCCLogEntry[]
	}
}

export default class ZnifferManager extends TypedEventEmitter<ZnifferManagerEventCallbacks> {
	private zniffer: Zniffer

	private config: ZnifferConfig

	private socket: SocketServer

	private error: string

	private restartTimeout: NodeJS.Timeout

	get started() {
		return !!this.zniffer?.active
	}

	constructor(config: ZnifferConfig, socket: SocketServer) {
		super()

		this.config = config
		this.socket = socket

		if (!config.enabled) {
			logger.info('Zniffer is DISABLED')
			return
		}

		const znifferOptions: ZnifferOptions = {
			convertRSSI: config.convertRSSI,
			defaultFrequency: config.defaultFrequency,
			logConfig: {
				enabled: config.logEnabled,
				level: config.logLevel ? loglevels[config.logLevel] : 'info',
				logToFile: config.logToFile,
				filename: ZNIFFER_LOG_FILE,
				forceConsole: isDocker() ? !config.logToFile : false,
				maxFiles: config.maxFiles || 7,
				nodeFilter:
					config.nodeFilter && config.nodeFilter.length > 0
						? config.nodeFilter.map((n) => parseInt(n))
						: undefined,
			},
		}

		parseSecurityKeys(config, znifferOptions)

		this.zniffer = new Zniffer(config.port, znifferOptions)
		this.zniffer.on('error', (error) => {
			this.onError(error)
		})

		this.zniffer.on('frame', (frame, rawData) => {
			const socketFrame = this.parseFrame(frame, rawData)

			this.socket
				.to('znifferFrames')
				.emit(socketEvents.znifferFrame, socketFrame)
		})

		this.zniffer.on('corrupted frame', (frame, rawData) => {
			const socketFrame = this.parseFrame(frame, rawData)

			this.socket
				.to('znifferFrames')
				.emit(socketEvents.znifferFrame, socketFrame)
		})

		this.zniffer.on('ready', () => {
			logger.info('Zniffer ready')
		})

		logger.info('Initing Zniffer...')
		this.init().catch(() => {})
	}

	private async init() {
		try {
			await this.zniffer.init()
		} catch (error) {
			logger.info('Retrying in 5s...')
			this.restartTimeout = setTimeout(() => {
				this.init().catch(() => {})
			}, 5000)
		}

		this.onStateChange()
	}

	private parseFrame(
		frame: Frame | CorruptedFrame,
		rawData: Uint8Array<ArrayBuffer>,
		timestamp = Date.now(),
	): SocketFrame {
		const socketFrame: SocketFrame = {
			...frame,
			corrupted: !('protocol' in frame),
			payload: '' as any,
			timestamp,
			raw: buffer2hex(rawData),
		}

		if ('payload' in frame) {
			if (frame.payload instanceof CommandClass) {
				socketFrame.parsedPayload = this.ccToLogRecord(frame.payload)
			} else {
				socketFrame.payload = buffer2hex(frame.payload)
			}
		}

		return socketFrame
	}

	private onError(error: Error) {
		logger.error('Zniffer error:', error)
		this.error = error.message
		this.onStateChange()
	}

	private onStateChange() {
		this.socket
			.to('znifferState')
			.emit(socketEvents.znifferState, this.status())
	}

	private checkReady() {
		if (!this.config.enabled || !this.zniffer) {
			throw new Error('Zniffer is not initialized')
		}
	}

	public status() {
		return {
			error: this.error,
			started: this.started,
			supportedFrequencies: Object.fromEntries(
				this.zniffer?.supportedFrequencies ?? [],
			),
			frequency: this.zniffer?.currentFrequency,
			lrRegions: Array.from(this.zniffer?.lrRegions ?? []),
			supportedLRChannelConfigs: Object.fromEntries(
				this.zniffer?.supportedLRChannelConfigs ?? [],
			),
			lrChannelConfig: this.zniffer?.currentLRChannelConfig,
		}
	}

	public getFrames() {
		this.checkReady()

		return this.zniffer.capturedFrames.map((frame) => {
			return this.parseFrame(
				frame.parsedFrame,
				Buffer.from(frame.frameData),
				frame.timestamp.getTime(),
			)
		})
	}

	public async setFrequency(frequency: number) {
		this.checkReady()

		logger.info(`Setting Zniffer frequency to ${frequency}`)
		await this.zniffer.setFrequency(frequency)

		this.onStateChange()

		logger.info(`Zniffer frequency set to ${frequency}`)
	}

	public async setLRChannelConfig(channelConfig: number) {
		this.checkReady()

		logger.info(
			`Setting Zniffer LR channel configuration to ${channelConfig}`,
		)
		await this.zniffer.setLRChannelConfig(channelConfig)

		this.onStateChange()

		logger.info(`Zniffer LR channel configuration set to ${channelConfig}`)
	}

	private ccToLogRecord(commandClass: CommandClass): Record<string, any> {
		try {
			const parsed: Record<string, any> = commandClass.toLogEntry()

			if (isEncapsulatingCommandClass(commandClass)) {
				parsed.encapsulated = [
					this.ccToLogRecord(commandClass.encapsulated),
				]
			} else if (isMultiEncapsulatingCommandClass(commandClass)) {
				parsed.encapsulated = [
					commandClass.encapsulated.map((cc) =>
						this.ccToLogRecord(cc),
					),
				]
			}
			return parsed
		} catch (error) {
			logger.error('Error parsing command class:', error)
			return {
				error: error.message,
			}
		}
	}

	public async close() {
		if (this.restartTimeout) clearTimeout(this.restartTimeout)

		if (this.zniffer) {
			this.zniffer.removeAllListeners()
			await this.stop()
			await this.zniffer.destroy()
		}
	}

	public async start() {
		this.checkReady()

		if (this.started) {
			logger.info('Zniffer already started')
			return
		}

		logger.info('Starting...')
		await this.zniffer.start()

		this.onStateChange()

		logger.info('Started')
	}

	public async stop() {
		this.checkReady()

		if (!this.started) {
			logger.info('Zniffer is already stopped')
			return
		}

		logger.info('Stopping...')
		await this.zniffer.stop()

		this.onStateChange()

		logger.info('Stopped')
	}

	public clear() {
		this.checkReady()

		logger.info('Clearing...')
		this.zniffer.clearCapturedFrames()

		logger.info('Frames cleared')
	}

	public async loadCaptureFromBuffer(buffer: Buffer) {
		this.checkReady()

		logger.info(`Loading capture from buffer (${buffer.length} bytes)`)

		try {
			// eslint-disable-next-line @typescript-eslint/ban-ts-comment
			// @ts-expect-error
			await this.zniffer.loadCaptureFromBuffer(buffer)

			logger.info(`Successfully loaded capture`)
		} catch (error) {
			logger.error('Error loading capture:', error)
			return {
				error: `Failed to load capture: ${(error as Error).message}`,
			}
		}
	}

	public async saveCaptureToFile() {
		this.checkReady()

		const filePath = ZNIFFER_CAPTURE_FILE.replace(
			'%DATE%',
			new Date().toISOString(),
		)
		logger.info(`Saving capture to ${filePath}`)
		await this.zniffer.saveCaptureToFile(filePath)
		logger.info('Capture saved')

		// Read the saved file to return its content for download
		const data = await readFile(filePath)

		return {
			path: filePath,
			name: basename(filePath),
			data: data,
		}
	}
}
