import fs from "node:fs/promises";
import path from "node:path";
import { parse } from "@babel/parser";
import { glob } from "glob";
import { normalizePath } from "vite";
import { TracedEventEmitter } from "@superblocksteam/shared";
import { applyErrorHandling, } from "./errors/error-handler.js";
import { CUSTOM_COMPONENTS_DIRECTORY } from "./file-system-helpers.js";
import { traverse } from "./traverse.js";
import { getLogger } from "./util/logger.js";
/**
 * Known component registration functions that we should detect.
 */
const COMPONENT_REGISTRATION_FUNCTIONS = ["registerComponent"];
/**
 * This class is responsible for managing the components in the project.
 * It is used to get the name of the component in the file, and to listen to changes in the components.
 */
export class ComponentsManager extends TracedEventEmitter {
    // path -> names (now supporting multiple components per file)
    registeredComponentNames = {};
    // path -> file contents
    fileContents = {};
    static instance;
    static setInstance(tracer) {
        ComponentsManager.instance = new ComponentsManager(tracer);
        return ComponentsManager.instance;
    }
    static getInstance() {
        if (!ComponentsManager.instance) {
            throw new Error("ComponentsManager should have been initialized during plugin init");
        }
        return ComponentsManager.instance;
    }
    constructor(tracer) {
        super(tracer, { captureRejections: true });
        applyErrorHandling(this, {
            initialize: { operation: "A component was changed, added, or removed" },
        });
    }
    async initialize(rootPath, watcher) {
        const projectFiles = await glob(path.join(rootPath, CUSTOM_COMPONENTS_DIRECTORY, "**/*.tsx"), {
            ignore: ["node_modules/**", "dist/**", "build/**"],
        });
        for (const file of projectFiles) {
            const normalizedFile = normalizePath(file);
            const fileContents = await fs.readFile(file, "utf-8");
            const componentNames = await this.getComponentNameFromFile(fileContents);
            if (componentNames && componentNames.length > 0) {
                this.registeredComponentNames[normalizedFile] = componentNames;
                this.fileContents[normalizedFile] = fileContents;
            }
        }
        // we only auto-register components in the components folder
        const componentsFolder = path.join(rootPath, CUSTOM_COMPONENTS_DIRECTORY);
        watcher.add(componentsFolder);
        watcher.on("unlink", (path) => {
            void this.onRemoveComponent(normalizePath(path));
        });
        watcher.on("add", (path) => {
            void this.onAddComponent(normalizePath(path));
        });
        watcher.on("change", (path) => {
            void (async () => {
                const normalizedPath = normalizePath(path);
                if (!normalizedPath.endsWith(".tsx") ||
                    !normalizedPath.startsWith(componentsFolder))
                    return;
                const existingFileContents = this.fileContents[normalizedPath];
                // This means we have not found a name for this component yet, even though the file exists, so
                // its like the add path
                if (!existingFileContents) {
                    void this.onAddComponent(normalizedPath);
                    return;
                }
                try {
                    const newFileContents = await fs.readFile(normalizedPath, "utf-8");
                    if (existingFileContents !== newFileContents) {
                        this.fileContents[normalizedPath] = newFileContents;
                        const newComponentNames = await this.getComponentNameFromFile(newFileContents);
                        const existingNames = this.registeredComponentNames[normalizedPath] || [];
                        // Handle removed components
                        if (existingNames.length > 0 &&
                            (!newComponentNames || newComponentNames.length === 0)) {
                            existingNames.forEach((name) => this.emit("registeredComponentRemoved", name));
                            delete this.registeredComponentNames[path];
                            delete this.fileContents[path];
                            return;
                        }
                        if (newComponentNames && newComponentNames.length > 0) {
                            // Detect removed components (were in existingNames but not in newComponentNames)
                            const removedComponents = existingNames.filter((name) => !newComponentNames.includes(name));
                            removedComponents.forEach((name) => this.emit("registeredComponentRemoved", name));
                            this.registeredComponentNames[normalizedPath] = newComponentNames;
                            newComponentNames.forEach((componentName) => {
                                this.emit("registeredComponentChanged", componentName);
                            });
                        }
                    }
                }
                catch {
                    // we don't care if this fails
                }
            })();
        });
    }
    onAddComponent = async (path) => {
        if (!path.endsWith(".tsx"))
            return;
        try {
            const fileContents = await fs.readFile(path, "utf-8");
            const componentNames = await this.getComponentNameFromFile(fileContents);
            if (componentNames && componentNames.length > 0) {
                this.registeredComponentNames[path] = componentNames;
                this.fileContents[path] = fileContents;
                componentNames.forEach((name) => this.emit("registeredComponentAdded", name));
            }
        }
        catch {
            // we don't care if this fails
        }
    };
    onRemoveComponent = (path) => {
        if (!path.endsWith(".tsx"))
            return;
        const componentNames = this.registeredComponentNames[path] || [];
        delete this.registeredComponentNames[path];
        delete this.fileContents[path];
        componentNames.forEach((name) => this.emit("registeredComponentRemoved", name));
    };
    isCustomComponentFile(path) {
        return path in this.registeredComponentNames;
    }
    isCustomComponent(componentName) {
        return Object.values(this.registeredComponentNames).some((names) => names.includes(componentName));
    }
    getComponentName(path) {
        // Return the first component name for backwards compatibility
        const names = this.registeredComponentNames[path];
        return names?.[0];
    }
    getComponentNames(path) {
        return this.registeredComponentNames[path] || [];
    }
    getComponentImportPath(componentName, rootPath) {
        const filePath = Object.keys(this.registeredComponentNames).find((filePath) => this.registeredComponentNames[filePath]?.includes(componentName));
        if (!filePath) {
            return;
        }
        // Normalize to forward slashes for import paths (cross-platform compatibility)
        return normalizePath(path.relative(rootPath, filePath)).replace(/\.tsx$/, "");
    }
    /**
     * Extracts all component names from a file containing registerComponent() calls.
     * Returns an array of component names, or undefined if none found.
     */
    async getComponentNameFromFile(fileContents) {
        if (!COMPONENT_REGISTRATION_FUNCTIONS.some((fn) => fileContents.includes(fn))) {
            return undefined;
        }
        try {
            const ast = parse(fileContents, {
                sourceType: "module",
                plugins: ["typescript", "jsx"],
            });
            const componentNames = [];
            traverse(ast, {
                CallExpression(nodePath) {
                    const { callee, arguments: args } = nodePath.node;
                    if (callee.type === "Identifier" &&
                        COMPONENT_REGISTRATION_FUNCTIONS.includes(callee.name)) {
                        const firstArg = args[0];
                        if (firstArg?.type === "Identifier") {
                            const name = firstArg.name;
                            componentNames.push(name);
                        }
                    }
                },
            });
            return componentNames.length > 0 ? componentNames : undefined;
        }
        catch (e) {
            getLogger().warn("Error parsing component names:", e);
            return undefined;
        }
    }
    get componentFilePaths() {
        return Object.keys(this.registeredComponentNames);
    }
    isInComponentDirectory(filePath) {
        return this.componentFilePaths.some((componentPath) => {
            if (componentPath === filePath) {
                return false;
            }
            const componentDir = path.dirname(componentPath);
            const fileDir = path.dirname(filePath);
            const relativePath = path.relative(componentDir, fileDir);
            return (relativePath === "" ||
                (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
        });
    }
}
//# sourceMappingURL=components-manager.js.map