import { camelize, EventEmitter, Observable } from '@matter/main';
import { Behavior, ClusterBehavior, type MutableEndpoint } from '@matter/node';
import type { ISYDeviceNode } from 'isy-nodejs/Devices/ISYDeviceNode';
import { DeviceNode, ISY, type CompositeDevice, type ObjectToUnion } from 'isy-nodejs/ISY';
import { ISYDevice } from 'isy-nodejs/ISYDevice';
import { ISYNode } from 'isy-nodejs/ISYNode';
import type { StringKeyOf } from 'type-fest';
import winston from 'winston';
import { MappingRegistry, type ClusterMapping, type DeviceToClusterMap } from '../Mappings/MappingRegistry.js';

export class ISYBridgedDeviceBehavior<N extends ISYDevice.Any, E extends MutableEndpoint = MutableEndpoint> extends Behavior {
	static override readonly id = 'isyNode';

	static override readonly early = true;

	listeners = [] as ((...any) => void)[];

	declare internal: ISYBridgedDeviceBehavior.Internal;
	declare state: ISYBridgedDeviceBehavior.State;

	declare events: ISYBridgedDeviceBehavior.Events & ISYBridgedDeviceBehavior.EventsFor<N>;

	override async initialize(_options?: {}) {
		await super.initialize(_options);
		var address = this.state.address;
		const cd = ISY.instance.getDevice(this.state.address);

		this.internal.device = cd;
		//@ts-ignore
		this.internal.map = MappingRegistry.getMapping(this.internal.device);

		this.logger.debug(`Initializing ${this.constructor.name} for ${this.internal.device.constructor.name} ${this.internal.device.name} with address ${address}`);
		if (ISYDevice.isComposite(cd)) {
			for (const m in cd.nodes) {
				const d = cd.nodes[m] as ISYNode<N['family']>;
				for (const f in d.drivers) {
					let evt = `${d.drivers[f].name}Changed`;
					const obs = Observable<[{ node: string; driver: string; newValue: any; oldValue: any; formattedValue: string }]>();

					const listener = (driver: string, newValue: any, oldValue: any, formattedValue: string) => obs.emit({ node: m, driver: driver, newValue, oldValue, formattedValue });
					d.events.on(evt, listener);
					this.events[`${m}.${evt}`] = obs;

					//@ts-ignore
					//d.events.on(evt, (driver: string, newValue: any, oldValue: any, formattedValue: string) => this.events.emit(evt, { driver, newValue, oldValue, formattedValue } as unknown as any));
				}
			}
		} else if (ISYDevice.isNode(cd)) {
			const d = cd;

			for (const f in d.drivers) {
				let evt = `${d.drivers[f].name}Changed`;
				const obs = Observable<[{ driver: string; newValue: any; oldValue: any; formattedValue: string }]>();

				d.events.on(evt, (driver: string, newValue: any, oldValue: any, formattedValue: string) => obs.emit({ driver, newValue, oldValue, formattedValue }));
				this.events[evt] = obs;

				//@ts-ignore
				//d.events.on(evt, (driver: string, newValue: any, oldValue: any, formattedValue: string) => this.events.emit(evt, { driver, newValue, oldValue, formattedValue } as unknown as any));
			}
		}
		this.logger.debug(`Initialized ${this.constructor.name} for ${this.internal.device.constructor.name} ${this.internal.device.name} with address ${address}`);
	}

	get logger() {
		if (!this.internal.logger) {
			let l = winston.loggers.get('matter').child({ address: this.device.address, name: this.device.label });
			const label = `${this.device.label} (${this.device.address})`;

			l.format = winston.format.label({ label: label });
			this.internal.logger = l;
		}
		return this.internal.logger;
	}

	get device(): N {
		return this.internal.device as N;
	}

	get map(): DeviceToClusterMap<N, E> {
		return (this.internal.map ??= MappingRegistry.getMapping<N, E>(this.device));
	}

	mapForBehavior<B extends ClusterBehavior, D extends ISYDevice.Any>(behavior: B): ClusterMapping<B['cluster'], D> {
		// @ts-ignore
		return this.map.mapping[camelize(behavior.cluster.name, false) as any] as any;
	}

	handlePropertyChange(driver: string, newValue: any, oldValue: any, formattedValue: string) {
		this.events.propertyChanged.emit({ driver, newValue, oldValue, formattedValue });
	}

	override [Symbol.asyncDispose]() {
		this.internal.device = null;
		//(this.internal.device.events as EventEmitter).removeAllListeners

		return super[Symbol.asyncDispose]();
	}
}

export namespace ISYBridgedDeviceBehavior {
	export class Internal {
		device?: ISYDevice.Any;
		map?: DeviceToClusterMap<typeof this.device, any>;

		logger?: winston.Logger;
	}

	type InternalEventsFor<N extends ISYNode<any, any, any, any>> =
		N extends ISYDeviceNode<any, infer D, any> ?
			{
				[s in keyof D as `${D[s]['name']}Changed`]: Observable<[{ driver: s; newValue: any; oldValue: any; formattedValue: string }]>;
			}
		:	never;

	export type InternalEventsForComposite<I extends CompositeDevice.Any> = ObjectToUnion<{
		[s in StringKeyOf<I['drivers']>]: {
			[q in keyof I['drivers'][s] as `${s}.${I['drivers'][s][q]['name']}Changed`]: Observable<[{ node: s; driver: q; newValue: any; oldValue: any; formattedValue: string }]>;
		};
	}>;

	export type EventsFor<N extends ISYDevice.Any> =
		N extends DeviceNode.Any ? InternalEventsFor<N>
		: N extends CompositeDevice.Any ? InternalEventsForComposite<N>
		: never;

	export class Events extends EventEmitter {
		propertyChanged = Observable<[{ node?: string; driver: string; newValue: any; oldValue: any; formattedValue: string }]>();
	}

	export class State {
		address = '';
	}
}
