import type {
	BuilderModule,
	ComponentSettings,
	Module,
} from '@nitrogenbuilder/types';

import { BatchFetchContext, type BatchTransport } from './BatchFetchContext.js';
import { logger } from './utils/logger.js';
import { updateModuleProps } from './utils/updateWithDefaultProps.js';

export class Nitrogen {
	static instance: Nitrogen;
	static modules = new Map<string, Module>();
	static moduleComponents = new Map<string, any>();
	static moduleDataFetchers = new Map<string, (props: any, ctx?: BatchFetchContext) => Promise<any>>();
	private static moduleEventQueued = false;
	private static nitrogenInitializedListenerAdded = false;

	// The idea hereis that the moduleData map key is a cache key. The first item in the array is always the moduleId.
	// This logic is not yet implemented. Right now it only works with one array item which is the moduleId.
	static moduleData = new Map<string, any>();

	constructor() {
		if (Nitrogen.instance) {
			return Nitrogen.instance;
		}
		Nitrogen.instance = this;
	}

	getModule(moduleId: string | number) {
		return Nitrogen.modules.get(`${moduleId}`);
	}

	getModuleComponent(moduleId: string | number) {
		return Nitrogen.moduleComponents.get(`${moduleId}`);
	}

	getModuleData(moduleId: string | number) {
		logger.log(
			'getting Nitrogen.moduleData, ' + moduleId,
			{ context: { moduleData: Nitrogen.moduleData.get(`${moduleId}`) } }
		);
		return Nitrogen.moduleData.get(`${moduleId}`);
	}

	getModuleDataFetcher(moduleId: string | number) {
		return Nitrogen.moduleDataFetchers.get(`${moduleId}`);
	}

	async getRequestedData({
		page,
		dynamicData,
		batchTransport,
	}: {
		page: BuilderModule[];
		dynamicData?: any;
		batchTransport?: BatchTransport;
	}) {
		// Step 1: Walk tree to collect all modules with data fetchers
		type DataModule = { moduleId: string; moduleName: string; props: any };
		const dataModules: DataModule[] = [];

		function collectDataModules(modules: BuilderModule[]) {
			for (const module of modules) {
				if (module.module) {
					const moduleName =
						typeof module.module === 'object'
							? module.module.name
							: module.module;

					if (Nitrogen.moduleDataFetchers.has(moduleName)) {
						const updatedModule = updateModuleProps(module, dynamicData);
						dataModules.push({
							moduleId: module.id,
							moduleName,
							props: updatedModule.props,
						});
					}
				}

				if (module.props?.children) {
					if (Array.isArray(module.props.children)) {
						collectDataModules(module.props.children);
					} else if (typeof module.props.children === 'object') {
						for (const slot in module.props.children) {
							collectDataModules(module.props.children[slot]);
						}
					}
				}
			}
		}

		collectDataModules(page);

		// Step 2: Create batch context if transport provided
		const ctx = batchTransport
			? new BatchFetchContext(batchTransport)
			: undefined;

		// Step 3: Start all getData calls concurrently
		const promises = dataModules.map(({ moduleId, moduleName, props }) => {
			const fetcher = Nitrogen.moduleDataFetchers.get(moduleName)!;
			return fetcher(
				{
					...props,
					__dynamicData: dynamicData,
				},
				ctx
			).then((data) => ({
				moduleId,
				data,
			}));
		});

		// Step 4: If batch context exists, flush the single batch request
		if (ctx) {
			await ctx.flush();
		}

		// Step 5: Wait for all getData post-processing to complete
		const results = await Promise.all(promises);

		// Step 6: Store results for this request and update the shared module cache.
		const requestedData: Record<string, any> = {};
		for (const { moduleId, data } of results) {
			requestedData[moduleId] = data;
			Nitrogen.moduleData.set(`${moduleId}`, data);
		}

		return requestedData;
	}

	registerModuleDataFetcher(
		moduleId: string | number,
		getData: (props: any, ctx?: BatchFetchContext) => Promise<any>
	) {
		Nitrogen.moduleDataFetchers.set(`${moduleId}`, getData);
	}

	private dispatchModulesEvent() {
		if (typeof window === 'undefined') return;

		const dispatch = () => {
			window.dispatchEvent(
				new CustomEvent('nitrogen-modules', {
					detail: {
						modules: Object.fromEntries(Nitrogen.modules),
					},
				})
			);
		};

		if (window?.nitrogen?.parentInit) {
			dispatch();
			return;
		}

		if (Nitrogen.nitrogenInitializedListenerAdded) return;

		Nitrogen.nitrogenInitializedListenerAdded = true;
		window.addEventListener(
			'nitrogen-initialized',
			() => {
				Nitrogen.nitrogenInitializedListenerAdded = false;
				dispatch();
			},
			{ once: true }
		);
	}

	private queueModulesEvent() {
		if (typeof window === 'undefined' || Nitrogen.moduleEventQueued) return;

		Nitrogen.moduleEventQueued = true;
		const flush = () => {
			Nitrogen.moduleEventQueued = false;
			this.dispatchModulesEvent();
		};

		if (typeof queueMicrotask === 'function') {
			queueMicrotask(flush);
		} else {
			setTimeout(flush, 0);
		}
	}

	registerModule(
		moduleId: string | number,
		Component: any,
		module: ComponentSettings,
		getData?: (props: any, ctx?: BatchFetchContext) => Promise<any>
	) {
		Nitrogen.modules.set(`${moduleId}`, {
			...module,
			name: `${moduleId}`,
			id: moduleId,
		} as Module); // overwriting the type so we can sneak in the id field
		Nitrogen.moduleComponents.set(`${moduleId}`, Component);
		if (getData) {
			Nitrogen.moduleDataFetchers.set(`${moduleId}`, getData);
		}

		logger.log('Nitrogen.modules', {
			context: { moduleId, moduleCount: Nitrogen.modules.size, windowType: typeof window },
		});

		this.queueModulesEvent();
	}
}

export const nitrogen = new Nitrogen();
