import { Logger } from 'winston';
import { Driver } from './Definitions/Global/Drivers.js';
import { Family } from './Definitions/Global/Families.js';
import { UnitOfMeasure } from './Definitions/Global/UOM.js';
import { type Message, Feature, ISY } from './ISY.js';

import type { Merge, UnionToIntersection } from '@matter/general';
import { CliConfigSetLevels } from 'winston/lib/winston/config/index.js';
import { Converter } from './Converters.js';
import type { Command } from './Definitions/Global/Commands.js';
import { Event } from './Definitions/Global/Events.js';
import type { CompositeDevice } from './Devices/CompositeDevice.js';
import { NodeFactory } from './Devices/NodeFactory.js';
import type { DriverState } from './Model/DriverState.js';
import { NodeInfo } from './Model/NodeInfo.js';
import type { NodeNotes } from './Model/NodeNotes.js';
import { NodeType } from './NodeType.js';
import type { ISYScene } from './Scenes/ISYScene.js';
import type { Factory as BaseFactory, MaybeWithUOM } from './Utils.js';
import { type ObjectToUnion, type StringKeys } from './Utils.js';
import type { Jsonify } from 'type-fest';
import type { JsonifyObject } from 'type-fest/source/jsonify.js';
import type { EventEmitter } from 'stream';

//type DriverValues<DK extends string | number | symbol,V = any> = {[x in DK]?:V};

export class ISYNode<
	T extends Family = Family,
	D extends ISYNode.DriverSignatures = {},
	C extends ISYNode.CommandSignatures = {},
	E extends ISYNode.EventSignatures = {},
> {
	// #region Properties (32)

	static #displayNameFunction: Function;

	#parentNode: ISYNode<any, any, any, any>;

	public readonly address: string;
	public readonly baseLabel: string;
	public readonly flag: any;
	public readonly isy: ISY;
	public readonly nodeDefId: string;

	public static family: Family;
	public static nodeDefId = 'Unknown';

	public static implements: string[] = [];
	public commands: Command.ForAll<C>;
	//public readonly formatted: DriverValues<keyof D,string> = {};
	//public readonly uom: { [x in Driver.Literal]?: UnitOfMeasure } = { ST: UnitOfMeasure.Boolean };
	//public readonly pending: DriverValues<keyof D> = {};
	//public readonly local: DriverValues<keyof D> = {};
	public drivers: Driver.ForAll<D> = {} as Driver.ForAll<D>;
	public enabled: boolean;
	//TODO: add signature for non-command/non-driver events
	public events: Event.EventEmitter<D, E>;
	//Event.FunctionSigFor<Event.ForAll<E,typeof this>> & Omit<EventEmitter,'on'>
	/*{
		[x in E]: x extends keyof D ? {name:`${D[x]["name"]}Changed`, driver: x, value: D[x]["value"], formatted: string, uom: UnitOfMeasure}
		: x extends keyof C ? {name: `${C[x]['name']}Triggered`, command: x}
		: {name: E};
	};*/
	public family: T;
	public folder: string = '';
	public hidden: boolean;
	public isDimmable: boolean;
	public isLoad: boolean;
	public label: string;
	public lastChanged: Date;
	public location: string;
	public logger: (msg: any, level?: keyof CliConfigSetLevels, ...meta: any[]) => Logger;
	// [x: string]: any;
	public name: string;
	public nodeType: number;
	public parent: any;
	public parentAddress: any;
	public parentType: NodeType;
	public propsInitialized: boolean;
	public scenes: ISYScene[];
	public spokenName: string;
	public type: any;

	public features: Feature;

	// #endregion Properties (32)

	// #region Constructors (1)

	constructor(isy: ISY, node: NodeInfo) {
		this.isy = isy;
		this.isy.nodes.set(node.address, this);
		this.nodeType = 0;
		this.flag = node.flag;
		this.nodeDefId = node.nodeDefId;
		this.address = String(node.address);

		this.name = node.name;
		this.family = node.family as T;

		this.parent = node.parent;

		this.parentType = Number(this.parent?.type);

		this.enabled = node.enabled ?? true;

		this.propsInitialized = false;
		const s = this.name.split('.');
		//if (s.length > 1)
		//s.shift();
		this.baseLabel = s
			.join(' ')
			.replace(/([A-Z])/g, ' $1')
			.replace('  ', ' ')
			.replace('  ', ' ')
			.trim();
		if (this.parentType === NodeType.Folder) {
			this.folder = isy.folderMap.get(this.parent._);
			isy.logger.debug(`${this.name} is in folder ${this.folder}`);
			this.logger = (msg: any, level: keyof CliConfigSetLevels = 'debug', ...meta: any[]) => {
				isy.logger[level](`${this.folder} ${this.name} (${this.address}): ${msg}`, meta);
				return isy.logger;
			};

			this.label = `${this.folder} ${this.baseLabel}`;
		} else {
			this.label = this.baseLabel;
			this.logger = (msg: any, level: keyof CliConfigSetLevels = 'debug', ...meta: any[]) => {
				isy.logger[level](`${this.name} (${this.address}): ${msg}`, meta);
				return isy.logger;
			};
		}

		this.events = new Event.NodeEventEmitter<this, D, C, E>(this) as Event.EventEmitter<D, E>;
		this.lastChanged = new Date();
	}

	// #endregion Constructors (1)

	// #region Public Getters And Setters (1)

	public get parentNode(): ISYNode<any, any, any, any> {
		if (this.#parentNode === undefined) {
			if (this.parentAddress !== this.address && this.parentAddress !== null && this.parentAddress !== undefined) {
				this.#parentNode = this.isy.getDevice(this.parentAddress) as unknown as ISYNode<any, any, any, any>;
				if (this.#parentNode !== null) {
					//this.#parentNode.addChild(this);
				}
			}
			this.#parentNode = null;
		}
		return this.#parentNode;
	}

	// #endregion Public Getters And Setters (1)

	// #region Public Methods (18)

	public addLink(isyScene: ISYScene) {
		this.scenes.push(isyScene);
	}

	_initialized: boolean = false;
	public get initialized(): boolean {
		if (!this._initialized) {
			for (const prop in this.drivers) if (this.drivers[prop]?.initialized == false && prop != 'ERR') return false;
		}
		this._initialized = true;
		return true;
	}

	public applyStatus(prop: DriverState) {
		try {
			var d = this.drivers?.[prop.id];

			if (d) {
				d.apply(prop);

				this.logger(`Property ${d?.label ?? prop.id} (${prop.id}) refreshed to: ${d.value} (${prop.formatted}})`);
				//d.state.value = this.convertFrom(prop.value, prop.uom, prop.id);
				//d.state.formatted = prop.formatted;
				//d.state.uom = prop.uom;
			} else {
				this.logger(`Driver ${prop.id} not found, creating new driver`);
				//@ts-expect-error
				this.drivers[prop.id] = Driver.create(prop.id as never, this as any, prop, {
					uom: prop.uom,
					label: (prop.name as string) ?? (prop.id as string),
					name: (prop.name as string) ?? (prop.id as string),
				});
			}
		} catch (e) {
			this.logger(e?.message ?? e, 'error');
		}
	}

	public convert(value: any, from: UnitOfMeasure, to: UnitOfMeasure): any {
		if (from === to) return value;
		else {
			try {
				return Converter.Standard[from][to].from(value);
			} catch {
				this.isy.logger.error(`Conversion from ${UnitOfMeasure[from]} to ${UnitOfMeasure[to]} not supported.`);
			} finally {
				return value;
			}
		}
	}

	public convertFrom(value: any, uom: UnitOfMeasure, propertyName?: StringKeys<D>): any {
		if (this.drivers[propertyName]?.uom != uom) {
			this.logger(
				`Converting ${this.drivers[propertyName].label} to ${UnitOfMeasure[this.drivers[propertyName]?.uom]} from ${UnitOfMeasure[uom]}`
			);
			return this.convert(value, uom, this.drivers[propertyName].uom);
		}
	}

	public convertTo(value: any, uom: UnitOfMeasure, propertyName?: StringKeys<D>) {
		if (this.drivers[propertyName]?.uom != uom) {
			this.isy.logger.debug(
				`Converting ${this.drivers[propertyName].label} from ${UnitOfMeasure[this.drivers[propertyName].uom]} to ${UnitOfMeasure[uom]}`
			);
			return this.convert(value, uom, this.drivers[propertyName].uom);
		}
	}

	public emit(
		event: 'propertyChanged' | 'controlTriggered',
		propertyName?: string,
		newValue?: any,
		oldValue?: any,
		formattedValue?: string,
		controlName?: string
	) {
		//if ('PropertyChanged') return super.emit(event, propertyName, newValue, oldValue, formattedValue);
		//else if ('ControlTriggered') return super.emit(event, controlName);
	}

	public generateLabel(template: string): string {
		// tslint:disable-next-line: only-arrow-functions
		if (!ISYNode.#displayNameFunction) {
			// template = template.replace("{", "{this."};
			const regex = /(?<op1>\w+) \?\? (?<op2>\w+)/g;
			this.logger(`Display name format: ${template}`);
			let newttemp = template.replace(
				regex,
				"this.$<op1> === null || this.$<op1> === undefined || this.$<op1> === '' ? this.$<op2> : this.$<op1>"
			);
			this.logger(`Template format updated to: ${newttemp}`);
			const s = {
				location: this.location ?? '',
				folder: this.folder ?? '',
				spokenName: this.spokenName ?? this.name,
				name: this.name ?? '',
			};
			newttemp = newttemp.replace('this.name', 'this.baseLabel');
			ISYNode.#displayNameFunction = new Function(`return \`${newttemp}\`.trim();`);
		}

		return ISYNode.#displayNameFunction.call(this);
	}

	public async getNotes(): Promise<NodeNotes> {
		try {
			const result = await this.isy.sendRequest(`nodes/${this.address}/notes`, {
				trailingSlash: false,
				errorLogLevel: 'silly',
				validateStatus(status) {
					return true;
				},
			});
			if (result !== null && result !== undefined) {
				return result.NodeProperties;
			} else {
				return null;
			}
		} catch (e) {
			return null;
		}
	}

	public handleControlTrigger(controlName: keyof E & keyof C): boolean {
		//this.lastChanged = new Date();
		//this.events.emit(`${this.commands[controlName].name}`, controlName);
		return true;
	}

	public handleEvent(event: Message<any>): boolean {
		let actionValue = null,
			formattedValue = null,
			uom = null,
			prec = null;
		if (event.action instanceof Object) {
			actionValue = event.action._;
			uom = event.action.uom;
			prec = event.action.prec;
		} else if (typeof event.action == 'number' || typeof event.action == 'string') {
			actionValue = Number(event.action);
		}

		if (event.control in this.drivers) {
			// property not command
			formattedValue = 'fmtAct' in event ? event.fmtAct : actionValue;

			return this.handlePropertyChange(
				event.control as StringKeys<D>,
				actionValue,
				uom ?? UnitOfMeasure.Unknown,
				prec,
				formattedValue
			);
		} else if (event.control === '_3') {
			this.logger(`Received Node Change Event: ${JSON.stringify(event)}.`, 'debug');
		} else {
			// this.logger(event.control);
			const e = event.control;
			const dispName = this.commands?.[e]?.name;
			if (dispName !== undefined && dispName !== null) {
				this.logger(`Command ${dispName} (${e}) event received.`);
			} else {
				this.logger(`Command ${e} event received.`);
			}
			this.handleControlTrigger(e);
			return true;
		}
	}

	public handlePropertyChange(
		propertyName: StringKeys<D>,
		value: any,
		uom: UnitOfMeasure,
		prec?: number,
		formattedValue?: string
	): boolean {
		this.lastChanged = new Date();
		let driver = this.drivers?.[propertyName];
		/*this.logger(`Driver ${propertyName} (${driver?.label} value update ${value} (${formattedValue}) uom: ${UnitOfMeasure[uom]} event received.`);*/

		if (driver?.patch(value, formattedValue, uom, prec)) {
			//this.emit('propertyChanged', propertyName, value, oldValue, formattedValue);
			this.scenes?.forEach(element => {
				this.logger(`Recalulating ${element.deviceFriendlyName}`);
				element.recalculateState();
			});
		}

		return true;
	}

	/*public override on(event: 'PropertyChanged', listener: (propertyName: keyof D, newValue: any, oldValue: any, formattedValue: string) => any): this;
	public override on(event: 'ControlTriggered', listener: (controlName: keyof C) => any): this;
	public override on(event: string | symbol, listener: (...args: any[]) => void): this {
		super.on(event, listener);
		return this;
	}*/
	public parseResult(node: { property: DriverState | DriverState[] }) {
		if (Array.isArray(node.property)) {
			for (const prop of node.property) {
				this.applyStatus(prop);
			}
		} else if (node.property) {
			this.applyStatus(node.property);
			//device.local[node.property.id] = node.property.value;
			//device.formatted[node.property.id] = node.property.formatted;
			//device.uom[node.property.id] = node.property.uom;
		}
	}

	public async readProperties(): Promise<DriverState[]> {
		var result = await this.isy.sendRequest(`nodes/${this.address}/status`);
		this.logger(JSON.stringify(result), 'debug');
		return result.property;
	}

	/*public addChild<K extends ISYDeviceNode<T, any, any, any>>(childDevice: K) {
    this.children.push(childDevice);
  }*/
	public async readProperty(propertyName: keyof D & string): Promise<DriverState> {
		var result = await this.isy.sendRequest(`nodes/${this.address}/${propertyName}`);
		return result.property;
	}

	public async refreshState(): Promise<any> {
		const device = this;
		const node = (await this.isy.sendRequest(`nodes/${this.address}/status`)).node;
		// this.logger(node);
		this.parseResult(node);
		return node;
	}

	public async refreshNotes() {
		const that = this;
		try {
			const result = await this.getNotes();
			if (result !== null && result !== undefined) {
				that.location = result.location ?? this.folder ?? '';
				that.spokenName = result.spoken ?? this.name ?? '';
				if (result.isLoad) this.features &= Feature.HasLoad;

				// if(result.spoken)
			} else {
				//that.logger('No notes found.','debug');
			}
			that.label = that.generateLabel.bind(that)(that.isy.displayNameFormat);
			that.label = that.label ?? this.baseLabel;
			that.logger(`The friendly name updated to: ${that.label}`);
		} catch (e) {
			that.logger(e);
		}
	}
	public async sendCommand(command: StringKeys<C>): Promise<any>;
	public async sendCommand(
		command: StringKeys<C>,
		value: MaybeWithUOM,
		parameters: Record<string | symbol, MaybeWithUOM | undefined>
	): Promise<any>;
	public async sendCommand(command: StringKeys<C>, value: MaybeWithUOM): Promise<any>;
	public async sendCommand(
		command: StringKeys<C>,
		parameters: Record<string | symbol, MaybeWithUOM | undefined>
	): Promise<any>;
	async sendCommand(
		command: StringKeys<C>,
		valueOrParameters?: MaybeWithUOM | Record<string | symbol, MaybeWithUOM | undefined>,
		parameters?: Record<string | symbol, MaybeWithUOM | undefined>
	): Promise<any> {
		if (valueOrParameters === null || valueOrParameters === undefined) {
			return this.isy.sendNodeCommand(this, command);
		}

		if (
			typeof valueOrParameters === 'string' ||
			typeof valueOrParameters === 'number' ||
			typeof valueOrParameters === 'boolean' ||
			Array.isArray(valueOrParameters)
		) {
			return this.isy.sendNodeCommand(this, command, valueOrParameters, parameters);
		}
		if (typeof valueOrParameters === 'object' && !Array.isArray(valueOrParameters)) {
			return this.isy.sendNodeCommand(this, command, null, { ...valueOrParameters, ...parameters });
		}

		///return this.isy.sendNodeCommand(this, command, valueOrParameters, { ...parameters });
	}

	public async updateProperty(propertyName: string, value: any): Promise<any> {
		var l = this.drivers[propertyName];
		if (l) {
			if (l.serverUom) l.state.pendingValue = this.convert(value, l.uom, l.serverUom);
			else l.state.pendingValue = value;
		}
		this.logger(`Updating property ${l.label}. incoming value: ${value} outgoing value: ${l.state.pendingValue}`);

		return this.isy.sendRequest(`nodes/${this.address}/set/${propertyName}/${l.state.pendingValue}`).then(p => {
			l.state.pendingValue = null;
		});
	}

	toJSON() {
		return {
			...this,

			initialized: this.initialized,
			family: Family[this.family],
			type: NodeType[this.type],
			scenes: this.scenes?.map(s => {
				return { address: s.address, name: s.name };
			}),
			commands: this.commands
				? Object.entries(this.commands).map(([id, c]) => {
						return {
							id: id,
							name: c.name,
						};
					})
				: [],
			isy: undefined,
			parent: this.parent ? { address: this.parent.address, name: this.parent.name } : undefined,
		};
	}

	// #endregion Public Methods (18)
}

export type Flatten<T, Level extends Number = 2, K = keyof T> = UnionToIntersection<
	T extends Record<string, unknown>
		? K extends string
			? T[K] extends Record<string, unknown>
				? keyof T[K] extends string
					? { [x in `${K}.${keyof T[K]}`]: T[K][TakeLast<x>] }
					: never
				: never
			: never
		: never
>;

type Split<X> = X extends `${infer A}.${infer B}` ? [A, ...Split<B>] : never;
type TakeLast<X> = X extends `${infer A}.${infer B}` ? TakeLast<B> : X;

type Test = Flatten<{ a: { b: { c: string } } }>;

export type DriverMap<T extends NodeList> = Flatten<{ [x in keyof T]: DriversOf<T[x]> }>;

export type NodeList = { [x: string]: ISYNode<any, any, any, any> };

export type DriversOf<T> = T extends ISYNode<any, infer D, infer C, infer E> ? D : never;

export type CommandsOf<T> = T extends ISYNode<any, any, infer C, any> ? C : never;

export type EventsOf<T> = T extends ISYNode<any, any, any, infer E> ? E : never;

export namespace ISYNode {
	export type FromSignatures<T> = T extends DriverSignatures ? Driver.ForAll<T> : never;

	export interface Factory<F extends Family, T extends ISYNode<F, any, any, any> = ISYNode<F, any, any, any>>
		extends BaseFactory<T> {
		Commands;
		Drivers;
	}

	type InternalDriversOf<T> = T extends ISYNode<any, infer D, any, any> ? D : never;

	export type DriversOf<T> =
		T extends ISYNode<any, any, any, any>
			? InternalDriversOf<T>
			: T extends CompositeDevice<any, any>
				? T['drivers']
				: never;
	export type CommandsOf<T> = T extends ISYNode<any, any, any, any> ? T['commands'] : never;
	export type EventsOf<T> = T extends ISYNode<any, any, any, infer E> ? E : never;
	export type FamilyOf<T> = T extends ISYNode<infer F, any, any, any> ? F : never;

	export type DriverTypesOf<T> = ObjectToUnion<DriversOf<T>>;

	export type CommandTypesOf<T extends ISYNode> = ObjectToUnion<CommandsOf<T>>;

	export type EventTypesOf<T extends ISYNode> = ObjectToUnion<EventsOf<T>>;

	export type EventNamesOf<T extends ISYNode> = EventTypesOf<T> extends { name: infer U } ? U : never;

	export type DriverNamesOf<T> = T extends { Drivers }
		? keyof T['Drivers']
		: DriverTypesOf<T> extends { name: infer U }
			? U
			: DriversOf<T> extends { name: infer U }
				? U
				: never;

	export type DriverKeysOf<T> = keyof DriversOf<T>;

	export type CommandKeysOf<T> = keyof CommandsOf<T>;

	export type CommandNamesOf<T> = T extends { Commands } ? keyof T['Commands'] : never;
	export type List = NodeList;

	export type DriverMap<T extends NodeList> = Flatten<{
		[x in keyof T]: DriversOf<T[x]>;
	}>;

	export type CommandMap<T extends NodeList> = Flatten<{
		[x in keyof T]: CommandsOf<T[x]>;
	}>;

	export type EventMap<T extends NodeList> = Flatten<{
		[x in keyof T]: EventsOf<T[x]>;
	}>;

	export type DriverSignatures = Record<string, Driver.Signature<UnitOfMeasure, any, UnitOfMeasure, string, string>>;

	export type CommandSignatures = Partial<{
		[x: string]: Command.Signature<any, any, any>;
	}>;

	export type EventSignatures = Record<string, Event.Signature>;

	export function getImplements(node: ISYNode<any, any, any, any> | typeof ISYNode): string[] {
		return NodeFactory.getImplements(node);
	}

	//TODO: fix return types
	/*export type WithCommands<C extends Command.Signatures<any>> = C extends Command.Signatures<infer U> ? {
      [K in C[U]["name"]]: C[K];
    } : never;*/

	/*export const With = <K extends Family, D extends DriverSignatures, C extends CommandSignatures, T extends Constructor<ISYNode<K, any, any, any>>>(base: T, drivers: D, commands: C) => {
		return class extends base implements Omit<ISYNode<K, D, C>, 'events'> {
			declare drivers: Driver.ForAll<D, false>;
			declare commands: Command.ForAll<C>;
		};
	};*/

	export type WithDrivers<D extends DriverSignatures> =
		D extends Driver.Signatures<infer U extends keyof D>
			? {
					[K in D[U] as K['name']]: K['value'];
				}
			: never;
}
