import { LogFormat, LogLevel, logLevelFromString, MatterFlowError, Logger as MatterLogger, Time } from '@matter/general';
import { ClusterBehavior, Endpoint, EndpointServer, Environment, MutableEndpoint, ServerNode, StorageService, VendorId, type EndpointLifecycle, type ServerEndpointInitializer } from '@matter/main';
import { GeneralCommissioning } from '@matter/main/clusters';
import { AggregatorEndpoint } from '@matter/main/endpoints/aggregator';
import { logEndpoint, type CommissioningMode } from '@matter/main/protocol';
import { QrCode } from '@matter/main/types';
import '@matter/nodejs';
import chalk from 'chalk';

import { createHash } from 'crypto';
import { appendFileSync, existsSync, writeFileSync } from 'fs';
import type { Devices, Factory, Family } from 'isy-nodejs';
import { DeviceNode, emphasize, includes, ISY, some, writeDebugFile } from 'isy-nodejs';
import { KeypadButton } from 'isy-nodejs/Devices/Insteon/index';
import { ISYDevice } from 'isy-nodejs/ISYDevice';
import type { ISYNode } from 'isy-nodejs/ISYNode';
import 'isy-nodejs/Utils';
import path from 'path';
import { format, loggers, transports } from 'winston';
import { BehaviorRegistry } from '../Behaviors/BehaviorRegistry.js';
import '../Behaviors/Insteon/index.js';
import { SlowGeneralCommissioningBehavior } from '../Behaviors/SlowCommissioningBehavior.js';
import { getRequiredBehaviors } from '../Behaviors/Utils.js';
import '../Behaviors/ZigBee/index.js';
import '../Mappings/index.js';
import { MappingRegistry } from '../Mappings/MappingRegistry.js';

// #region Interfaces (1)

export let instance: ServerNode;

export interface Config {
	// #region Properties (8)

	discriminator: number;
	passcode: number;
	port: number;
	productId: number;
	productName?: string;
	uniqueId: string;
	vendorId: number;
	vendorName?: string;

	ipv4?: boolean;

	ipv6?: boolean;

	deviceConfig?: DeviceConfig[];

	excludeDevicesByDefault?: boolean;

	logLevels?: {
		'iox-matter'?: ServerLogLevel;
		'matter.js'?: ServerLogLevel;
		isy: ServerLogLevel;
	};

	// #endregion Properties (8)
}

export type ServerLogLevel = 'silly' | 'debug' | 'info' | 'warn' | 'error';
export interface DeviceConfig {
	applyTo:
		| {
				device: string | string[];
		  }
		| { family: string | string[] }
		| { nodeDef: string | string[] }
		| { family: keyof typeof Family | (keyof typeof Family)[] }
		| { address: string | string[] }
		| { deviceType: string | string[] }
		| { predicate: (node: ISYDevice.Any) => boolean };
	options:
		| {
				exclude: true;
		  }
		| {
				include: true;
				label?: string;
				//@ts-expect-error
				mappings?: { [x in Devices[keyof Devices]]: DeviceToClusterMap<unknown, any> };
		  };
}

// #endregion Interfaces (1)

// #region Functions (3)

let t: MutableEndpoint;

export async function create(isy?: ISY, config?: Config): Promise<ServerNode> {
	let s = await createMatterServer(isy, config);

	return s;
}

export function appliesTo(device: ISYDevice.Any, deviceOptions: DeviceConfig): boolean {
	if ('device' in deviceOptions.applyTo) {
		return some((x) => device.constructor.name === x, ...deviceOptions.applyTo.device);
	} else if ('nodeDef' in deviceOptions.applyTo) {
		if (ISYDevice.isNode(device)) {
			return some((x) => device.nodeDefId.startsWith(x), ...deviceOptions.applyTo.nodeDef);
		}
	} else if ('address' in deviceOptions.applyTo) {
		return includes(deviceOptions.applyTo.address, device.address);
	} else if ('deviceType' in deviceOptions.applyTo && 'typeCode' in device) {
		return some((x) => device.typeCode.startsWith(x), ...deviceOptions.applyTo.deviceType);
	} else if ('predicate' in deviceOptions.applyTo) {
		return deviceOptions.applyTo.predicate(device);
	}
	return false;
}
const deviceOptionsCache: { [x: string]: DeviceConfig['options'] } = {};
export function getDeviceOptions(node: ISYDevice.Any, ...configs: DeviceConfig[]): DeviceConfig['options'] {
	if (deviceOptionsCache[node.address]) {
		return deviceOptionsCache[node.address];
	}
	if (configs) {
		if (Array.isArray(configs)) {
			for (const config of configs) {
				if (appliesTo(node, config)) {
					deviceOptionsCache[node.address] = config.options; //TODO: rank by specificity
				}
			}
		}
		/*for (const options of deviceOptions) {
			if (appliesTo(node, options)) {
				return options.options; //TODO: rank by specificity
			}
		}*/
	}

	//deviceOptionsCache[node.address] = {exclude: false};
	return (deviceOptionsCache[node.address] ??= { include: true });
}

export let initialized = false;

export async function createMatterServer(isy?: ISY, config?: Config): Promise<ServerNode> {
	config.deviceConfig ??= [
		{
			applyTo: {
				predicate: (p) => p.label.toLowerCase().endsWith('slave')
			},
			options: {
				exclude: true
			}
		}
	];
	config.excludeDevicesByDefault ??= false;
	var logger = loggers.add('matter', {
		transports: isy.logger.transports,
		levels: isy.logger.levels,
		format: format.label({ label: chalk.cyan.bold('iox-matter') }),
		level: config.logLevels?.['iox-matter'] ?? 'debug'
	});
	if (isy === undefined) {
		isy = ISY.instance;
	}
	if (MatterLogger) {
		var loggermjs = loggers.add('matter.js', {
			transports: isy.logger.transports,
			levels: isy.logger.levels,
			format: format.label({ label: chalk.magenta.bold('matter.js') }),
			level: config.logLevels?.['matter.js'] ?? 'info'
		});
		var loggerendpoint = loggers.add('matter-endpoint', {
			transports: new transports.File({ filename: path.join(isy.storagePath, 'debug', 'matter-endpoint.log') }),
			levels: isy.logger.levels,
			format: format.combine(format.simple(), format.colorize()),
			level: 'info'
		});
	}
	try {
		MatterLogger.addLogger(
			'polyLogger',
			(lvl, message) => {
				let msg = message.slice(23).remove(LogLevel[lvl]).trimStart();
				let level = LogLevel[lvl].toLowerCase().replace('notice', 'info').replace('fatal', 'error');
				if (msg.startsWith('EndpointStructureLogger')) {
					loggerendpoint.log(level, msg);
					//if (lvl === LogLevel.INFO) level = 'debug';
				} else {
					loggermjs.log(level, msg);
				}
			},
			/*Preserve existing formatting, but trim off date*/
			{
				defaultLogLevel: logLevelFromString(config.logLevels?.['matter.js'] ?? 'info'),

				logFormat: 'plain'
			}
		);
		if (existsSync(path.join(isy.storagePath, 'debug', 'matter-endpoint.log'))) {
			writeFileSync(path.join(isy.storagePath, 'debug', 'matter-endpoint.log'), '');
		}

		/*MatterLogger.addLogger(
			'EndpointStructureLogger',
			(lvl, message) => {
				if(lvl === LogLevel.INFO) {
					appendFileSync(`matter-endpoint.log`, message);
			},
			{ defaultLogLevel: logLevelFromString('info'), logFormat: 'ansi' }
		);*/
	} finally {
		try {
			//MatterLogger.defaultLogLevel = logLevelFromString('debug');
			if (MatterLogger.getLoggerforIdentifier('default') !== undefined) {
				MatterLogger.removeLogger('default');
			}
		} catch {}
	}

	config = await initializeConfiguration(isy, config);

	logger.info(`Matter config: ${JSON.stringify(config)}`);

	/*Remove existing logging*/
	let s = ServerNode.RootEndpoint;
	let server = await ServerNode.create(s.with(SlowGeneralCommissioningBehavior), {
		// Required: Give the Node a unique ID which is used to store the state of this node
		id: config.uniqueId,

		// Provide Network relevant configuration like the port
		// Optional when operating only one device on a host, Default port is 5540
		network: {
			port: 5550,
			discoveryCapabilities: {
				onIpNetwork: true
			}
		},

		// Provide Commissioning relevant settings
		// Optional for development/testing purposes
		commissioning: {
			passcode: config.passcode,
			discriminator: config.discriminator
		},
		generalCommissioning: {
			basicCommissioningInfo: {
				failSafeExpiryLengthSeconds: 60 * 5 /*as of Matter.js 0.12.0, default timeout is 60s*/,
				maxCumulativeFailsafeSeconds: 60 * 10
			}
		},

		// Provide Node announcement settings
		// Optional: If Ommitted some development defaults are used
		productDescription: {
			name: isy.model,
			deviceType: AggregatorEndpoint.deviceType
		},

		// Provide defaults for the BasicInformation cluster on the Root endpoint
		// Optional: If Omitted some development defaults are used
		basicInformation: {
			vendorName: isy.vendorName,
			vendorId: VendorId(config.vendorId),
			nodeLabel: config.productName,
			productName: config.productName,
			productLabel: config.productName,
			productId: config.productId,
			hardwareVersionString: isy.firmwareVersion,
			softwareVersionString: isy.firmwareVersion,
			serialNumber: isy.id.replaceAll(':', '-'),
			uniqueId: config.uniqueId
		}
	});

	logger.info(`Bridge server endpoint added`);

	/**
	 * Matter Nodes are a composition of endpoints. Create and add a single multiple endpoint to the node to make it a
	 * composed device. This example uses the OnOffLightDevice or OnOffPlugInUnitDevice depending on the value of the type
	 * parameter. It also assigns each Endpoint a unique ID to store the endpoint number for it in the storage to restore
	 * the device on restart.
	 *
	 * In this case we directly use the default command implementation from matter.js. Check out the DeviceNodeFull example
	 * to see how to customize the command handlers.
	 */

	const aggregator = new Endpoint(AggregatorEndpoint, { id: 'aggregator' });

	await server.add(aggregator);

	logger.info(`Bridge aggregator added`);
	let endpoints = 0;
	let devices: ISYDevice.Any[] = [];
	if (config.excludeDevicesByDefault) {
		let addresses = [] as string[];
		for (const opt in config.deviceConfig) {
			let deviceConfig = config.deviceConfig[opt];
			if ('address' in deviceConfig.applyTo) {
				if ('include' in deviceConfig.options) {
					if (typeof deviceConfig.applyTo.address === 'string') {
						addresses.push(deviceConfig.applyTo.address);
					} else if (Array.isArray(deviceConfig.applyTo.address)) {
						addresses.push(...deviceConfig.applyTo.address);
					}
				}
			}
		}
		for (const address of addresses) {
			devices.push(isy.getDevice(address));
		}
	}
	if (devices.length === 0) {
		devices = Array.from(isy.devices.values());
	}
	for (const node of devices) {
		let device = node as ISYDevice.Any & { refreshNotes: () => Promise<void> };
		/*if (device.parentAddress !== device.address) {
			continue;
		}*/
		try {
			let deviceOptions = getDeviceOptions(node, ...config.deviceConfig);

			if ('exclude' in deviceOptions) {
				logger.info(`Device excluded by config. ${node.label}`);
				continue;
			}
			let uniqueId = `${device.address.replaceAll(' ', '_').replaceAll('.', '_')}`;
			if (device.enabled) {
				await device.refreshNotes();
				if (!device.initialized) {
					if (ISYDevice.isQueryable(device)) {
						logger.info('Device not initialized. Querying...');
						await device.query();
						await device.refreshState();
					}
				}

				//const name = `OnOff ${isASocket ? "Socket" : "Light"} ${i}`;

				//@ts-ignore
				//of (DimmableLightDevice.with(BridgedDeviceBasicInformationServer, ISYBridgedDeviceBehavior, ISYOnOffBehavior, ISYDimmableBehavior)) | typeof (OnOffLightDevice.with(BridgedDeviceBasicInformationServer, ISYBridgedDeviceBehavior, ISYOnOffBehavior));*/
				let deviceType = MappingRegistry.getMapping(device)?.deviceType;

				let baseBehavior = deviceType;
				if (baseBehavior !== undefined) {
					let b = getRequiredBehaviors(deviceType);

					for (let s in b) {
						let behavior = b[s] as ClusterBehavior.Type;
						if (behavior.cluster && behavior.cluster.name !== 'Unknown') {
							let b = BehaviorRegistry.get(device, behavior.cluster.name);
							if (b) {
								baseBehavior = baseBehavior.with(b) as any;
							}
						}
					}

					/*if (DimmerLamp.isImplementedBy(device)) {
					baseBehavior = deviceType?.with(RelayOnOffBehavior, DimmerLevelControlBehavior);
					// if(device instanceof InsteonSwitchDevice)
					// {
					//     baseBehavior = DimmerSwitchDevice.with(BridgedDeviceBasicInformationServer);
				} else if (RelayLamp.isImplementedBy(device)) {
					baseBehavior = deviceType?.with(RelayOnOffBehavior);
					// if(device instanceof InsteonSwitchDevice)
					// {
					//     baseBehavior = OnOffLightSwitchDevice.with(BridgedDeviceBasicInformationServer);
					// }
				}*/

					if (ISYDevice.isNode(device)) logger.info(`Device ${device.label} (${device.address}) with NodeDefId = ${device.nodeDefId} mapped to ${deviceType.name}`);
					else logger.info(`${device.constructor?.name} ${device.label} (${device.address}) mapped to ${deviceType.name}`);
					//@ts-ignore
					const endpoint = new Endpoint(baseBehavior, {
						id: uniqueId,
						isyNode: {
							address: device.address
						},
						userLabel: {
							labelList: [
								{
									label: 'Room',
									value: device.location ?? 'Unspecified'
								}
							]
						},
						bridgedDeviceBasicInformation: {
							nodeLabel: device.label.rightWithToken(32),
							vendorName: device.manufacturer?.leftWithToken(32) ?? config.vendorName.leftWithToken(32),

							vendorId: VendorId(config.vendorId),

							productName: device.productName?.leftWithToken(32),
							partNumber: device.modelNumber?.leftWithToken(32),
							productLabel: device.model?.leftWithToken(64),

							hardwareVersion: !isNaN(Number(device.version)) ? Number(device.version) : 0,
							hardwareVersionString: `v.${device.version}`,
							softwareVersion: !isNaN(Number(device.version)) ? Number(device.version) : 0,
							softwareVersionString: `v.${device.version}`,
							//softwareVersion: Number(device.version),
							//hardwareVersionString: `v.${device.version}`,

							serialNumber: uniqueId.replaceAll('_', '.'),
							reachable: true,
							uniqueId: uniqueId
						}
					});

					await aggregator.add(endpoint);
					logger.info(`Endpoint added ${JSON.stringify(endpoint.id)} for ${device.label} (${device.address})`);
					endpoints++;
					//endpoints.push({0:endpoint,1:device});
				}
				//endpoint.lifecycle.ready.on(()=> device.initialize(endpoint as any));
			}
		} catch (e) {
			logger.error(`Error adding endpoint for ${device.label} (${device.address}): ${e.message}`);
		}

		/**
		 * Register state change handlers and events of the endpoint for identify and onoff states to react to the commands.
		 *
		 * If the code in these change handlers fail then the change is also rolled back and not executed and an error is
		 * reported back to the controller.
		 */
	}
	logger.info(emphasize(`${endpoints} endpoints added to bridge.`));
	/**
	 * In order to start the node and announce it into the network we use the run method which resolves when the node goes
	 * offline again because we do not need anything more here. See the Full example for other starting options.
	 * The QR Code is printed automatically.
	 */

	logger.info('Bringing bridge server endpoint online');
	await server.start();

	logger.info(emphasize('Bridge server endpoint is online'));
	/**
	 * Log the endpoint structure for debugging reasons and to allow to verify anything is correct
	 */

	//MatterLogger.setLogger("EndpointStructureLogger", ((level, message) => logger.log(Level[level], message)));

	//logEndpoint(EndpointServer.forEndpoint(server));
	//if(logger.isTraceEnabled())
	// logEndpoint(EndpointServer.forEndpoint(server), {logAttributePrimitiveValues: true, logAttributeObjectValues: true});
	//else if(logger.isDebugEnabled())
	// {

	logEndpoint(EndpointServer.forEndpoint(server), { logAttributePrimitiveValues: true, logAttributeObjectValues: true, logNotSupportedClusterAttributes: true, logClusterCommands: true, logClusterEvents: true, logClusterGlobalAttributes: false });
	// }
	if (server.lifecycle.online) {
		const { qrPairingCode, manualPairingCode } = server.state.commissioning.pairingCodes;

		logger.info('\n' + QrCode.get(qrPairingCode));
		logger.info(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
		logger.info(`Manual pairing code: ${manualPairingCode}`);
	}
	instance = server;
	instance.lifecycle.destroyed.once(() => {
		try {
			logger.info('Server endpoint destroyed.');
			//logger.info('Unhooking matter logger');
			//MatterLogger.removeLogger('polyLogger');

			//logger.close();
		} finally {
			instance = null;
		}
	});
	return server;
}

export type PairingCodeData = {
	qrPairingCode: string;
	manualPairingCode: string;
	renderedQrPairingCode: string;
	url: string | URL;
};

export function getPairingCode(server: ServerNode = instance): PairingCodeData {
	let codes = server.state.commissioning.pairingCodes as PairingCodeData;
	codes.renderedQrPairingCode = QrCode.get(codes.qrPairingCode);
	codes.url = `https://project-chip.github.io/connectedhomeip/qrcode.html?data=${codes.qrPairingCode}`;
	return codes;
}

async function initializeConfiguration(isy: ISY, config?: Config): Promise<Config> {
	var logger = isy.logger;

	const environment = Environment.default;
	const storageService = environment.get(StorageService);
	//storageService.factory = n => new StorageBackendMemory(n);
	const storagePath = path.resolve(isy.storagePath, 'server');

	environment.vars.set('storage.path', storagePath);
	environment.vars.set('runtime.signals', false);

	environment.vars.use(() => {
		storageService.location = storagePath;
	});

	logger.info(`Matter storage location: ${storageService.location} (Directory)`);

	const stor = await storageService.open('bridge');
	const deviceStorage = stor.createContext('data');

	if (config.passcode) {
		environment.vars.set('passcode', config.passcode);
	}

	if (config.discriminator) {
		environment.vars.set('discriminator', config.discriminator);
	}

	if (config.vendorId) {
		environment.vars.set('vendorid', config.vendorId);
	}

	if (config.productId) {
		environment.vars.set('productid', config.productId);
	}

	//environment.vars.set('uniqueid', isy.id.replaceAll(':', '_'));

	//logger.info(`Matter configuration: ${JSON.stringify(environment.vars)}`);

	const vendorName = isy.vendorName;
	const passcode = environment.vars.number('passcode') ?? (await deviceStorage.get('passcode', 20202021));
	const discriminator = environment.vars.number('discriminator') ?? (await deviceStorage.get('discriminator', 3840));
	// product name / id and vendor id should match what is in the device certificate
	const vendorId = environment.vars.number('vendorid') ?? (await deviceStorage.get('vendorid', 0xfff1));
	const productId = environment.vars.number('productid') ?? (await deviceStorage.get('productid', isy.productId));
	const productName = environment.vars.string('productname') ?? (await deviceStorage.get('productname', isy.productName));
	const port = environment.vars.number('port') ?? 5540;
	const uniqueId = environment.vars.string('uniqueid') ?? (await deviceStorage.get('uniqueid', createHash('md5').update(isy.id.replaceAll(':', '_')).update(Time.nowMs().toString()).digest('hex')));

	// Persist basic data to keep them also on restart
	await deviceStorage.set({
		passcode,
		discriminator,
		vendorid: vendorId,
		productid: productId,
		productName: productName,
		uniqueid: uniqueId
	});
	await stor.close();

	//storageService.factory = n => new StorageBackendMemory({});

	//ogger.info(`Matter storage service type: ${storageService.factory);

	return {
		//deviceName,
		vendorName,
		passcode,
		discriminator,
		vendorId,
		productName,
		productId,
		port,
		uniqueId,
		ipv4: config.ipv4 ?? true,
		ipv6: config.ipv6 ?? true,
		deviceConfig: config.deviceConfig
	};
}

// #endregion Functions (3)
