import { stat, mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { cwd } from 'node:process';
import type { ApiPackage } from '@discordjs/api-extractor-model';
import { ApiItem, ApiModel, ApiDeclaredItem, ApiItemContainerMixin, ApiItemKind } from '@discordjs/api-extractor-model';
import { generatePath } from '@discordjs/api-extractor-utils';
import { DocNodeKind, TSDocConfiguration } from '@microsoft/tsdoc';
import type { DocLinkTag, DocCodeSpan, DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import { request } from 'undici';

export interface MemberJSON {
	kind: string;
	name: string;
	path: string;
	summary: string | null;
}

export const PACKAGES = [
	'discord.js',
	'brokers',
	'builders',
	'collection',
	'core',
	'formatters',
	'next',
	'proxy',
	'rest',
	'util',
	'voice',
	'ws',
];
let idx = 0;

export function addPackageToModel(model: ApiModel, data: any) {
	let apiPackage: ApiPackage;
	if (data.metadata) {
		const tsdocConfiguration = new TSDocConfiguration();
		const tsdocConfigFile = TSDocConfigFile.loadFromObject(data.metadata.tsdocConfig);
		tsdocConfigFile.configureParser(tsdocConfiguration);

		apiPackage = ApiItem.deserialize(data, {
			apiJsonFilename: '',
			toolPackage: data.metadata.toolPackage,
			toolVersion: data.metadata.toolVersion,
			versionToDeserialize: data.metadata.schemaVersion,
			tsdocConfiguration,
		}) as ApiPackage;
	} else {
		apiPackage = ApiItem.deserializeDocgen(data, 'discord.js') as ApiPackage;
	}

	model.addMember(apiPackage);
	return model;
}

/**
 * Attempts to resolve the summary text for the given item.
 *
 * @param item - The API item to resolve the summary text for.
 */
export function tryResolveSummaryText(item: ApiDeclaredItem): string | null {
	if (!item?.tsdocComment) {
		return null;
	}

	const { summarySection } = item.tsdocComment;

	let retVal = '';

	// Recursively visit the nodes in the summary section.
	const visitTSDocNode = (node: DocNode) => {
		switch (node.kind) {
			case DocNodeKind.CodeSpan:
				retVal += (node as DocCodeSpan).code;
				break;
			case DocNodeKind.PlainText:
				retVal += (node as DocPlainText).text;
				break;
			case DocNodeKind.LinkTag:
				retVal += (node as DocLinkTag).urlDestination;
				break;
			case DocNodeKind.Section:
			case DocNodeKind.Paragraph: {
				for (const child of (node as DocParagraph).nodes) {
					visitTSDocNode(child);
				}

				break;
			}

			default: // We'll ignore all other nodes.
				break;
		}
	};

	for (const node of summarySection.nodes) {
		visitTSDocNode(node);
	}

	if (retVal === '') {
		return null;
	}

	return retVal;
}

export function visitNodes(item: ApiItem, tag: string) {
	const members: (MemberJSON & { id: number })[] = [];

	for (const member of item.members) {
		if (!(member instanceof ApiDeclaredItem)) {
			continue;
		}

		if (member.kind === ApiItemKind.Constructor || member.kind === ApiItemKind.Namespace) {
			continue;
		}

		if (ApiItemContainerMixin.isBaseClassOf(member)) {
			members.push(...visitNodes(member, tag));
		}

		members.push({
			id: idx++,
			name: member.displayName,
			kind: member.kind,
			summary: tryResolveSummaryText(member) ?? '',
			path: generatePath(member.getHierarchy(), tag),
		});
	}

	return members;
}

export async function writeIndexToFileSystem(
	members: ReturnType<typeof visitNodes>,
	packageName: string,
	tag = 'main',
) {
	const dir = 'searchIndex';

	try {
		(await stat(join(cwd(), 'public', dir))).isDirectory();
	} catch {
		await mkdir(join(cwd(), 'public', dir));
	}

	await writeFile(
		join(cwd(), 'public', dir, `${packageName}-${tag}-index.json`),
		JSON.stringify(members, undefined, 2),
	);
}

export async function fetchVersions(pkg: string) {
	const response = await request(`https://docs.discordjs.dev/api/info?package=${pkg}`);
	return response.body.json() as Promise<string[]>;
}

export async function fetchVersionDocs(pkg: string, version: string) {
	const response = await request(`https://docs.discordjs.dev/docs/${pkg}/${version}.api.json`);
	return response.body.json() as Promise<Record<any, any>>;
}

export async function generateAllIndices({
	fetchPackageVersions = fetchVersions,
	fetchPackageVersionDocs = fetchVersionDocs,
	writeToFile = true,
} = {}) {
	const indices: Record<any, any>[] = [];

	for (const pkg of PACKAGES) {
		const versions = await fetchPackageVersions(pkg);

		for (const version of versions) {
			idx = 0;

			const data = await fetchPackageVersionDocs(pkg, version);
			const model = addPackageToModel(new ApiModel(), data);
			const members = visitNodes(model.tryGetPackageByName(pkg)!.entryPoints[0]!, version);

			const sanitizePackageName = pkg.replaceAll('.', '-');
			const sanitizeVersion = version.replaceAll('.', '-');

			if (writeToFile) {
				await writeIndexToFileSystem(members, sanitizePackageName, sanitizeVersion);
			} else {
				indices.push({ index: `${sanitizePackageName}-${sanitizeVersion}`, data: members });
			}
		}
	}

	return indices;
}
