import { readdir } from "node:fs/promises";
import { extname, join } from "node:path";
import { pathToFileURL } from "node:url";
import type { ExtensionDefinition } from "./types";

function isExtensionDefinition(value: unknown): value is ExtensionDefinition {
    if (!value || typeof value !== "object") {
        return false;
    }
    const maybe = value as { id?: unknown; init?: unknown };
    return typeof maybe.id === "string" && typeof maybe.init === "function";
}

function resolveExtensionDefinition(
    loadedModule: Record<string, unknown>,
    sourceFile: string,
    label: string
): ExtensionDefinition {
    if (isExtensionDefinition(loadedModule.default)) {
        return loadedModule.default;
    }

    if (isExtensionDefinition(loadedModule.extension)) {
        return loadedModule.extension;
    }

    for (const exported of Object.values(loadedModule)) {
        if (isExtensionDefinition(exported)) {
            return exported;
        }
    }

    throw new Error(`${label} extension module did not export an ExtensionDefinition: ${sourceFile}`);
}

async function discoverExtensionsInDirectory(
    directoryPath: string,
    label: string
): Promise<ExtensionDefinition[]> {
    const entries = await readdir(directoryPath, { withFileTypes: true });
    const extensionFiles = entries
        .filter((entry) => entry.isFile())
        .map((entry) => entry.name)
        .filter((name) => name !== "index.ts" && name !== "index.js")
        .filter((name) => {
            const ext = extname(name);
            return ext === ".ts" || ext === ".js";
        })
        .sort((a, b) => a.localeCompare(b));

    const discovered: ExtensionDefinition[] = [];
    const seenIds = new Set<string>();

    for (const fileName of extensionFiles) {
        const modulePath = join(directoryPath, fileName);
        const loadedModule = (await import(pathToFileURL(modulePath).href)) as Record<
            string,
            unknown
        >;
        const extension = resolveExtensionDefinition(loadedModule, fileName, label);
        if (seenIds.has(extension.id)) {
            throw new Error(`Duplicate ${label.toLowerCase()} extension id discovered: ${extension.id}`);
        }
        seenIds.add(extension.id);
        discovered.push(extension);
    }

    return discovered;
}

export async function discoverExtensionsFromDirectories(
    directoryPaths: string[],
    label: string
): Promise<ExtensionDefinition[]> {
    const combined: ExtensionDefinition[] = [];
    const seenIds = new Set<string>();

    for (const directoryPath of directoryPaths) {
        const extensions = await discoverExtensionsInDirectory(directoryPath, label);
        for (const extension of extensions) {
            if (seenIds.has(extension.id)) {
                throw new Error(
                    `Duplicate ${label.toLowerCase()} extension id across directories: ${extension.id}`
                );
            }
            seenIds.add(extension.id);
            combined.push(extension);
        }
    }

    return combined;
}
