import type {
	Category,
	ComponentManifest,
	ComponentManifestCategory,
	ComponentManifestChildren,
	ComponentManifestEntry,
	ComponentManifestGroup,
	ComponentManifestOption,
	ComponentManifestProp,
	ComponentSettings,
	Prop,
} from '@nitrogenbuilder/types';

export interface ComponentManifestSource {
	name: string;
	scope?: string;
	settings: ComponentSettings;
}

type Serializable =
	| null
	| boolean
	| number
	| string
	| Serializable[]
	| { [key: string]: Serializable };

const PROP_COPY_KEYS: Array<keyof ComponentManifestProp> = [
	'label',
	'default',
	'responsive',
	'divider',
	'clearable',
	'conditions',
	'allowOther',
	'options',
	'accept',
	'ctrlType',
	'useUnit',
	'step',
	'sources',
	'dialogTitle',
	'collection',
	'initialLimitDefault',
	'totalLimitDefault',
	'loadMoreLimitDefault',
	'useLoadMoreDefault',
	'usePaginationDefault',
	'config',
	'hasToolbar',
	'defaultLinkState',
	'layout',
	'layoutMode',
	'startLabel',
	'endLabel',
	'min',
	'max',
];

function sanitizeValue(value: unknown): Serializable | undefined {
	if (value === undefined) return undefined;
	if (
		value === null ||
		typeof value === 'string' ||
		typeof value === 'number' ||
		typeof value === 'boolean'
	) {
		return value;
	}

	if (Array.isArray(value)) {
		return value
			.map((entry) => sanitizeValue(entry))
			.filter((entry) => entry !== undefined) as Serializable[];
	}

	if (typeof value === 'function') {
		return undefined;
	}

	if (typeof value === 'object') {
		const next: Record<string, Serializable> = {};

		for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
			const serialized = sanitizeValue(entry);
			if (serialized !== undefined) {
				next[key] = serialized;
			}
		}

		return next;
	}

	return undefined;
}

function sanitizeOptions(options: unknown): ComponentManifestOption[] | undefined {
	const serialized = sanitizeValue(options);
	if (!Array.isArray(serialized)) return undefined;
	return serialized as ComponentManifestOption[];
}

function normalizeProp(prop: Prop): ComponentManifestProp {
	const next: ComponentManifestProp = {
		type: prop.type,
	};

	for (const key of PROP_COPY_KEYS) {
		const value = sanitizeValue((prop as Record<string, unknown>)[key]);
		if (value !== undefined) {
			if (key === 'options') {
				next.options = sanitizeOptions(value);
				continue;
			}

				(next as unknown as Record<string, unknown>)[key] = value;
		}
	}

	if ('props' in prop && prop.props) {
		next.props = normalizePropMap(prop.props);
	}

	if (prop.type === 'asset-folder' && 'files' in prop) {
		next.meta = {
			assetCount: Object.keys(prop.files || {}).length,
		};
	}

	return next;
}

function normalizePropMap(props: Record<string, Prop>): Record<string, ComponentManifestProp> {
	return Object.fromEntries(
		Object.entries(props).map(([propKey, prop]) => [propKey, normalizeProp(prop)]),
	);
}

function normalizeGroup(groupKey: string, group: Category['groups'][string]): ComponentManifestGroup {
	return {
		key: groupKey,
		label: group.label,
		props: normalizePropMap(group.props),
	};
}

function normalizeCategory(categoryKey: string, category: Category): ComponentManifestCategory {
	return {
		key: categoryKey,
		label: category.label,
		groups: Object.fromEntries(
			Object.entries(category.groups).map(([groupKey, group]) => [
				groupKey,
				normalizeGroup(groupKey, group),
			]),
		),
	};
}

function normalizeChildren(children: unknown): ComponentManifestChildren | undefined {
	const serialized = sanitizeValue(children);
	if (serialized === undefined) return undefined;
	return serialized as ComponentManifestChildren;
}

export function createComponentManifestEntry({
	name,
	scope,
	settings,
}: ComponentManifestSource): ComponentManifestEntry {
	return {
		name,
		scope,
		description: settings.description,
		sidebarCategory: settings.options?.category,
		children: normalizeChildren(settings.options?.children),
		categories: Object.fromEntries(
			Object.entries(settings.categories).map(([categoryKey, category]) => [
				categoryKey,
				normalizeCategory(categoryKey, category),
			]),
		),
	};
}

export function createComponentManifest(
	components: ComponentManifestSource[],
	options?: {
		source?: string;
		generatedAt?: string;
	}
): ComponentManifest {
	return {
		components: [...components]
			.sort((a, b) => a.name.localeCompare(b.name))
			.map((component) => createComponentManifestEntry(component)),
		generatedAt: options?.generatedAt ?? new Date().toISOString(),
		source: options?.source,
	};
}
