import type { Constructor } from 'type-fest';
import type { Feature } from '../Definitions/Global/Features.js';
import { Category, Family } from '../Definitions/index.js';
import { isDynamic, ISY } from '../ISY.js';
import { ISYDevice } from '../ISYDevice.js';
import { ISYNode } from '../ISYNode.js';

import type { NodeInfo } from '../Model/NodeInfo.js';
import type { ISYScene } from '../Scenes/ISYScene.js';
import type { Factory as BaseFactory, InstanceOf, ObjectToUnion, StringKeys } from '../Utils.js';
import type { ISYDeviceNode } from './ISYDeviceNode.js';

export type CompositeDevice<F extends Family, N extends { [x: string]: ISYNode.Factory<F, any> }, R = N[keyof N]> = {
	[x in keyof N]: InstanceOf<N[x]>;
} & {
	root: R | null;

	events: { [x in keyof N]: InstanceType<N[x]['Class']>['events'] };

	drivers: { [x in keyof N]: InstanceType<N[x]['Class']>['drivers'] };

	commands: { [x in keyof N]: InstanceType<N[x]['Class']>['commands'] };
	nodes: { [x in keyof N]: InstanceType<N[x]['Class']> };

	applyNodeDefs(): Promise<void>;

	refreshNotes(): Promise<void>;

	addNode: (node: NodeInfo | ISYNode, isy?: ISY) => void | Promise<void>;
} & Omit<ISYDevice<F, unknown, unknown, unknown>, 'drivers' | 'commands' | 'events'>;

export namespace CompositeDevice {
	export type DriversOf<N extends CompositeDevice<any, any>> = N['drivers'];

	export type CommandsOf<N extends CompositeDevice<any, any>> = N['commands'];

	export type EventsOf<N extends CompositeDevice<any, any>> = N['events'];

	export type DriverNamesOf<N extends CompositeDevice<any, any>> = ObjectToUnion<{
		[x in StringKeys<DriversOf<N>>]: `${x}.${ISYNode.DriverNamesOf<N[x]> & string}`;
	}>;

	export type CommandNamesOf<N> =
		N extends BaseFactory<CompositeDevice<any, infer X>> ? CommandNamesOf<X[keyof X]> : never;

	export type EventNamesOf<N extends CompositeDevice<any, any>> = ObjectToUnion<{
		[x in StringKeys<DriversOf<N>>]: `${x}.${ISYNode.EventNamesOf<N[x]> & string}`;
	}>;

	export type DriverKeysOf<N extends CompositeDevice<any, any>> = ObjectToUnion<{
		[x in StringKeys<DriversOf<N>>]: `${x}.${ISYNode.DriverKeysOf<N[x]> & string}`;
	}>;

	export type CommandKeysOf<N extends CompositeDevice<any, any>> = ObjectToUnion<{
		[x in StringKeys<CommandsOf<N>>]: `${x}.${ISYNode.CommandKeysOf<N[x]> & string}`;
	}>;

	export type Any = CompositeDevice<Family, { [x: string]: ISYNode.Factory<Family, any> }>;

	//@ts-ignore
	type test = DriverNamesOf<FanDevice>;

	export function of<F extends Family, N extends { [x: string]: ISYNode.Factory<F, any> }>(
		nodes: N,
		keyFunction: (node: NodeInfo) => [keyof N, boolean]
	): Constructor<CompositeDevice<F, N>>;
	export function of<F extends Family, N extends { [x: string]: ISYNode.Factory<F, any> }>(
		nodes: N,
		keyMap: { [x in keyof N]: number | string }
	): Constructor<CompositeDevice<F, N>>;
	export function of<F extends Family, N extends { [x: string]: ISYNode.Factory<F, any> }>(
		nodes: N,
		keyFunction: { [x in keyof N]: number | string } | ((node: NodeInfo) => [keyof N, boolean])
	): Constructor<CompositeDevice<F, N> & ISYDevice<F, any, any, any>> {
		if (keyFunction === undefined) {
			keyFunction = (node: NodeInfo) => [node.name, true];
		}
		if (typeof keyFunction === 'function') {
			return CompositeOf(nodes, keyFunction as any);
		} else if (typeof keyFunction === 'object') {
			const keyIndex = keyFunction as { [x in keyof N]: number | string };
			return CompositeOf(nodes, (node: NodeInfo | ISYNode) => {
				for (const key in keyIndex) {
					if (node instanceof ISYNode) {
						// Handle ISYNode case if needed
					} else if (isDynamic(node) && node.nodeTypeId == `${keyIndex[key]}`) {
						return [key, node.pnode == node.address] as any;
					}
					if (node.address.endsWith(keyIndex[key].toString())) {
						return [key, keyIndex[key] == 1 || keyIndex[key] == '1'];
					}
				}
				// Return a default value if no match is found
				return [Object.keys(keyIndex)[0] as keyof N, false] as any;
			});
		}
		// This should never be reached, but TypeScript needs a return
		throw new Error('Invalid keyFunction parameter');
	}

	export interface Factory<
		F extends Family,
		N extends { [x: string]: ISYNode.Factory<F, any> },
		C extends CompositeDevice<F, N>,
	> extends BaseFactory<C> {
		//Drivers: { [x in keyof N]: N[x]['Drivers'] };
		//Commands: { [x in keyof N]: [N[x]['Commands']] };
		Nodes: N;
	}

	export function isComposite(device: ISYDevice<any, any, any, any>): device is CompositeDevice<any, any> {
		return 'addNode' in device;
	}
	//@ts-ignore
}

export function CompositeOf<F extends Family, N extends { [x: string]: ISYNode.Factory<F, any> }>(
	nodes: N,
	keyFunction: (node: NodeInfo | ISYNode) => [keyof N, boolean]
): Constructor<CompositeDevice<F, N>> {
	return class implements ISYDevice<F, any, any, any> {
		readonly isy: ISY;

		/*public static async create(...args: any[]): Promise<CompositeDevice<F, N>> {
			const d = new class(args) as any as CompositeDevice<F, N>;
			let isy = d.isy;
			for (const n in d.nodes) {
				const node = d.nodes[n] as ISYNode;
				if ('getNodeDef' in node && typeof node.getNodeDef == 'function' && 'applyNodeDef' in node && typeof node.applyNodeDef == 'function') {
				}
			}
			return d;
		}*/

		constructor(...args: any[]) {
			if (args[0] instanceof ISY) {
				this.isy = args.shift();

				for (const nodeInfo of args as NodeInfo[]) {
					this.addNode(nodeInfo, this.isy);
				}
			}
		}
		category: F extends Family.Insteon ? Category.Insteon : Category.Home.Category;
		deviceClass: any;
		enabled: boolean;
		family: F;
		hidden: boolean;
		isDimmable: boolean;
		label: string;
		model: string;
		modelNumber: string;
		name: any;
		parentAddress: any;
		productName: string;
		scenes: ISYScene[];
		subCategory: number;
		type: any;
		typeCode: string;
		version: string;
		manufacturer: string;
		productId: string | number;
		modelName: string;

		location: string;

		features: Feature;

		async query(): Promise<void> {
			for (const n in this.nodes) {
				let node = this.nodes[n];
				if (ISYDevice.isQueryable(node)) {
					await node.query();
				}
			}
		}

		async refreshState(): Promise<void> {
			for (const n in this.nodes) {
				let node = this.nodes[n];
				await node.refreshState();
			}
		}

		public async applyNodeDefs(): Promise<void> {
			for (const n in this.nodes) {
				let node = this.nodes[n] as ISYDeviceNode<F, any, any, any>;
				if (ISYDevice.isDynamic(node)) {
					await node.applyNodeDef();
				}
			}
		}

		_initialized = false;
		public get initialized(): boolean {
			if (!this._initialized)
				for (const key in this.nodes) {
					if (!this.nodes[key]?.initialized) {
						return false;
					}
				}
			this._initialized = true;
			return true;
		}

		public address: string;

		public events: { [x in keyof N]: InstanceOf<N[x]>['events'] } = {} as any;

		public drivers: { [x in keyof N]: InstanceType<N[x]['Class']>['drivers'] } = {} as any;

		public commands: { [x in keyof N]: InstanceType<N[x]['Class']>['commands'] } = {} as any;

		public nodes: { [x in keyof N]: InstanceType<N[x]['Class']> } = {} as any;

		public async refreshNotes(): Promise<void> {
			for (const key in this.nodes) {
				await this.nodes[key].refreshNotes();
			}
			return Promise.resolve();
		}
		public root: ISYDeviceNode<F, any, any, any> | null = null;

		public async addNode(node: ISYNode): Promise<void>;
		public async addNode(node: NodeInfo, isy: ISY): Promise<void>;
		public async addNode(node: NodeInfo | ISYNode, isy = this.isy) {
			let n: ISYDeviceNode<F, any, any, any> | null = null;
			if (node instanceof ISYNode) {
				n = node as unknown as ISYDeviceNode<any, any, any, any>;
			} else {
				const key = keyFunction(node)[0];
				const nodeFactory = nodes[key];
				if (nodeFactory?.create) {
					n = await nodeFactory.create(isy, node);
				} else {
					throw new Error(`No factory found for node key: ${String(key)}`);
				}
			}

			if (!n) {
				throw new Error('Failed to create node');
			}

			const keyL = keyFunction(node);
			const key = keyL[0];
			const isRoot = keyL[1];
			this.nodes[key] = n as InstanceType<N[typeof key]['Class']>;

			//@ts-ignore
			this[key] = n;

			Object.defineProperty(this.events, key, {
				get(): () => any {
					return this[key].events;
				},
			});
			Object.defineProperty(this.drivers, key, {
				get(): () => any {
					return this[key].drivers;
				},
			});
			Object.defineProperty(this.commands, key, {
				get(): () => any {
					return this[key].commands;
				},
			});
			if (isRoot) {
				this.address = node.address;
				this.family = n.family;
				this.category = n.category;
				this.deviceClass = n.deviceClass;
				this.enabled = n.enabled;
				this.hidden = n.hidden;
				this.isDimmable = n.isDimmable;
				this.label = n.label;
				this.model = n.model;
				this.modelNumber = n.modelNumber;
				this.name = n.name;
				this.parentAddress = n.parentAddress;
				this.productName = n.productName;
				this.scenes = n.scenes;
				this.subCategory = n.subCategory;
				this.type = n.type;
				this.typeCode = n.typeCode;
				this.version = n.version;
				this.manufacturer = n.manufacturer;
				this.productId = n.productId;
				this.modelName = n.modelName;
				this.manufacturer = n.manufacturer;
				this.location = n.location;
				this.root = n;
			}
		}
	} as unknown as Constructor<CompositeDevice<F, N>>;
}

/*

export class ISYMultiNodeDevice<T extends Family, N extends NodeList>
  implements ISYDevice<T, ISYNode.DriverMap<N>, ISYNode.CommandMap<N>, string> {
  commands: UnionToIntersection<{ [x in keyof N]: ISYNode.CommandsOf<N[x]>; } extends Record<string, unknown> ? keyof N extends string ? { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N] extends Record<string, unknown> ? keyof { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N] extends string ? { [x in `${string & keyof N}.${string & keyof { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N]}`]: { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N][x extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? any : B : B : B : B : B : B : B : B : B : B : x]; } : never : never : never : never>;
  readProperty(propertyName: keyof UnionToIntersection<{ [x in keyof N]: ISYNode.DriversOf<N[x]>; } extends Record<string, unknown> ? keyof N extends string ? { [x in keyof N]: ISYNode.DriversOf<N[x]>; }[string & keyof N] extends Record<string, unknown> ? keyof { [x in keyof N]: ISYNode.DriversOf<N[x]>; }[string & keyof N] extends string ? { [x in `${string & keyof N}.${string & keyof { [x in keyof N]: ISYNode.DriversOf<N[x]>; }[string & keyof N]}`]: { [x in keyof N]: ISYNode.DriversOf<N[x]>; }[string & keyof N][x extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? any : B : B : B : B : B : B : B : B : B : B : x]; } : never : never : never : never>): Promise<DriverState> {
    throw new Error('Method not implemented.');
  }
  sendCommand(command: Extract<keyof UnionToIntersection<{ [x in keyof N]: ISYNode.CommandsOf<N[x]>; } extends Record<string, unknown> ? keyof N extends string ? { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N] extends Record<string, unknown> ? keyof { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N] extends string ? { [x in `${string & keyof N}.${string & keyof { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N]}`]: { [x in keyof N]: ISYNode.CommandsOf<N[x]>; }[string & keyof N][x extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? B extends `${infer A}.${infer B}` ? any : B : B : B : B : B : B : B : B : B : B : x]; } : never : never : never : never>, string>, parameters?: Record<string | symbol, string | number> | string | number): Promise<any> {
    throw new Error('Method not implemented.');
  }

  updateProperty(propertyName: keyof ISYNode.DriverMap<N>, value: string): Promise<any> {
    throw new Error('Method not implemented.');
  }

  handleControlTrigger(controlName: string): boolean {
    throw new Error('Method not implemented.');
  }

  _parentDevice: ISYDevice<T, any, any, any>;
  children: ISYNode<any, any, any, any>[];
  convertTo(value: any, uom: number);
  convertTo(value: any, uom: number, propertyName: keyof DriverMap<N>);
  convertTo(value: unknown, uom: unknown, propertyName?: unknown): any {
    throw new Error('Method not implemented.');
  }
  convertFrom(value: any, uom: number);
  convertFrom(value: any, uom: number, propertyName: keyof DriverMap<N>);
  convertFrom(value: unknown, uom: unknown, propertyName?: unknown): any {
    throw new Error('Method not implemented.');
  }
  addLink(isyScene: ISYScene): void {
    throw new Error('Method not implemented.');
  }
  addChild(childDevice: ISYNode<any, any, any, any>): void {
    throw new Error('Method not implemented.');
  }

  readProperties(): Promise<DriverState[]> {
    throw new Error('Method not implemented.');
  }

  refresh(): Promise<any> {
    throw new Error('Method not implemented.');
  }
  refreshNotes(): Promise<void> {
    throw new Error('Method not implemented.');
  }
  parseResult(node: { property: DriverState | DriverState[]; }): void {
    throw new Error('Method not implemented.');
  }

  handlePropertyChange(propertyName: keyof ISYNode.DriverMap<N> & string, value: any, uom: UnitOfMeasure, prec: number, formattedValue: string): boolean {
    throw new Error('Method not implemented.');
  }

  logger(arg0: string): unknown {
    throw new Error('Method not implemented.');
  }
  handleEvent(evt: any): unknown {
    throw new Error('Method not implemented.');
  }
  on(arg0: string, arg1: any): unknown {
    throw new Error('Method not implemented.');
  }
  name: any;
  drivers: Driver.ForAll<ISYNode.DriverMap<N>>;
  address: string;
  family: T;
  typeCode: string;
  deviceClass: any;
  parentAddress: any;
  category: number;
  subCategory: number;
  type: any;
  scenes: ISYScene[];
  hidden: boolean;
  enabled: boolean;
  productName: string;
  model: string;
  modelNumber: string;
  version: string;
  isDimmable: boolean;
  label: string;

} */
