import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import archiver from "archiver";
import bodyParser from "body-parser";
import { glob } from "glob";
import { debounce } from "lodash-es";
import { normalizePath } from "vite";
import { ApiTriggerType, SUPERBLOCKS_LIVE_GIT_BRANCH, } from "@superblocksteam/shared";
import { extractInputsFromApi } from "./binding-extraction/extract-identifiers.js";
import { ImportError } from "./build-errors.js";
import { createClarkLogForwarder } from "./clark-log-forwarder.js";
import { ComponentsManager } from "./components-manager.js";
import { getDownloadArchiveFilename } from "./download-filename.js";
import { makeServerError } from "./errors/make-server-error.js";
import { isApiFilePath, SCOPE_FILE, isPageFilePath, getCodeModeDownloadArchiveGlobPatterns, isSkillFilePath, } from "./file-system-helpers.js";
import { FileSystemManager } from "./file-system-manager.js";
import { createGitOperationLock } from "./git-operation-lock.js";
import { indexHtmlMiddleware } from "./injected-index.js";
import { LockServiceStatus } from "./lock-service/index.js";
import { registerMigrationRoutes } from "./migration/migration-routes.js";
import { getImports } from "./parsing/imports.js";
import { getLegacyComponentsFolder, isCustomBuildEnabled, setOptions, } from "./plugin-options.js";
import { createIdResolver, getFullImportPath } from "./resolver.js";
import { parseRouterFile } from "./router-parser.js";
import { registerSnapshotRoutes } from "./snapshot-routes.js";
import { createSocketManager, disposeSocketManager } from "./socket-manager.js";
import { getAllPages } from "./util.js";
import { GenerationNumberSequence } from "./util/generation-number-sequence.js";
import { createGitIgnorePatterns } from "./util/ignore-patterns.js";
import { getErrorMeta, getLogger, setDefaultLogger } from "./util/logger.js";
import { resolvedVirtualRegisteredComponentsId, virtualLegacyId, resolvedVirtualLegacyId, virtualRegisteredComponentsId, } from "./virtual-modules.js";
import { YAML_TYPES_PLUGIN_NAME, yamlApiTypesPlugin, } from "./vite-plugin-yaml-types.js";
let hasStarted = false;
let isInitialized = false;
let config;
// Store file transform errors to send when editor clients connect
let pendingFileTransformError = null;
const loggedViteErrors = [];
export const PLUGIN_NAME = "sb-file-sync";
export function fileSyncVitePlugin(pluginParams, options) {
    if (!pluginParams) {
        // Noop stubs for the warm-standby pre-warm pass. Must mirror `enforce`
        // and `apply` on the real plugins below — Vite sorts plugins by those
        // fields before hashing the plugin list into the dep cache key, so a
        // mismatch would make activation discard the pre-warmed cache.
        return {
            plugin: [
                { name: PLUGIN_NAME, apply: "serve", enforce: "pre" },
                { name: YAML_TYPES_PLUGIN_NAME, enforce: "pre" },
            ],
            dispose: () => { },
        };
    }
    const logger = pluginParams.logger;
    // Set the default logger for all files that still use getLogger()
    setDefaultLogger(logger);
    setOptions(options ?? {
        legacyComponentsFolder: "legacy",
    });
    const generationNumberSequence = new GenerationNumberSequence();
    const syncService = pluginParams.syncService;
    const lockService = pluginParams.lockService;
    const aiService = pluginParams.aiService;
    const gitService = pluginParams.gitService;
    const activateGitService = pluginParams.activateGitService;
    const gitOperationLock = createGitOperationLock();
    const fsOperationQueue = pluginParams.fsOperationQueue;
    const httpServer = pluginParams.httpServer;
    let socketManager;
    let unregisterLogForwarder;
    const fileSyncManager = new FileSystemManager(fsOperationQueue, generationNumberSequence, pluginParams.tracer);
    if (syncService) {
        syncService.generationNumberSequence = generationNumberSequence;
    }
    // Set up sync callback in lock service
    if (lockService && syncService) {
        lockService.setSyncCallback(async () => {
            await syncService.uploadDirectory("initialSync");
            await syncService.uploadDirectoryNowIfNeeded("initialSync");
        });
    }
    // Set up generation complete callback in lock service
    if (aiService && lockService) {
        lockService.setWaitForGenerationCompleteCallback(async () => {
            await aiService.waitUntilGenerationComplete();
        });
    }
    // Wire up commit ID updates for recording sessions
    if (aiService && syncService) {
        syncService.on("commitIdChanged", (commitId) => {
            void aiService.getRecordingManager().setCurrentCommitId(commitId);
        });
    }
    const componentsManager = ComponentsManager.setInstance(pluginParams.tracer);
    const lastImportErrorFiles = new Set();
    let isFirstContentfulGeneration = true;
    let hasHadAnyDrafts = false;
    const debouncedFileUpdateHandlers = new Map();
    const addFileTransformError = (filePath, error) => {
        const fileTransformError = makeServerError(error, {
            critical: false,
        });
        if (error.stack) {
            const stackLines = error.stack.split("\n");
            const clampedStack = stackLines.length > 20
                ? stackLines.slice(0, 20).join("\n") + "\n... (stack truncated)"
                : error.stack;
            fileTransformError.stackTrace = clampedStack;
        }
        // Add specific advice for common Babel traversal errors
        if (error.message.includes("pass a scope and parentPath unless traversing a Program/File")) {
            fileTransformError.message = `File transformation error in ${filePath}: ${error.message}. This error commonly occurs when there are duplicate identifiers in scope destructuring. Check for repeated variable names in destructuring assignments.`;
        }
        pendingFileTransformError = { filePath, error };
    };
    const plugin = {
        name: PLUGIN_NAME,
        apply: "serve",
        enforce: "pre",
        config() {
            return {
                server: {
                    watch: {
                        ignored: ["**/.superblocks/**"],
                    },
                },
            };
        },
        async configureServer(server) {
            const tracer = pluginParams.tracer;
            config = server.config;
            // SDK/fullstack apps override `root` to "./client" in their
            // vite.config.ts, so config.root points to the client/ subdir.
            // Operations that need the full app tree use this instead.
            const appRootDir = pluginParams.appRootDir ?? config.root;
            // Normalized prefix for detecting server/* file changes.
            // Used to eagerly sync server files to DBFS on write so
            // ensureFilesSynced() is a no-op on the SDK API execution path.
            const serverPrefix = normalizePath(appRootDir) + "/server/";
            // Debounce timer for eager server sync — coalesces Clark's burst writes
            // (e.g. API file + index.ts barrel, 1-4s apart) into a single upload.
            let eagerServerSyncTimer;
            // Set up Vite module invalidation callback for SyncService
            // This ensures that when Clark writes files, Vite's module cache is invalidated
            if (syncService) {
                syncService.setModuleInvalidationCallback((filePath) => {
                    const fullPath = normalizePath(path.join(config.root, filePath));
                    const module = server.moduleGraph.getModuleById(fullPath);
                    if (module) {
                        server.moduleGraph.invalidateModule(module);
                        logger.info(`[vite-plugin] Invalidated module graph for: ${filePath}`);
                    }
                });
            }
            // Wrap logger.error to forward errors to AI and track them,
            // while downgrading Vite errors to info level for our own logger.
            // User notification is unaffected — Vite still sends vite:error via HMR.
            config.logger.error = (message, options) => {
                if (aiService?.isBusy()) {
                    void aiService.handleBuildSystemError(new Error(message));
                }
                if (options?.error) {
                    loggedViteErrors.push(options.error);
                }
                logger.info("[vite-plugin] Build system error", { message });
            };
            createIdResolver(server.config, server.environments.client);
            await Promise.all([
                tracer.startActiveSpan("componentsManagerInitialize", async (compSpan) => {
                    try {
                        await componentsManager.initialize(process.cwd(), server.watcher);
                    }
                    finally {
                        compSpan.end();
                    }
                }),
                server.waitForRequestsIdle("sb-file-sync").then(async () => {
                    if (hasStarted) {
                        // We only want to initialize the file sync once, but vite seems to fire this multiple times
                        return;
                    }
                    hasStarted = true;
                    const root = server.config.root;
                    try {
                        await tracer.startActiveSpan("fileSyncManagerWatch", async (watchSpan) => {
                            try {
                                await fileSyncManager.watch(server.watcher, root).then(() => {
                                    isInitialized = true;
                                });
                            }
                            finally {
                                watchSpan.end();
                            }
                        });
                    }
                    catch (e) {
                        logger.error("Error watching files", getErrorMeta(e));
                    }
                }),
            ]);
            if (aiService) {
                aiService.setSourceTracker(fileSyncManager.getSourceTracker());
                aiService.setFileSystemManager(fileSyncManager);
                aiService.setViteServer(server);
                aiService.setRuntimeGitServices(gitService, syncService, gitOperationLock);
            }
            server.middlewares.use((req, res, next) => {
                void indexHtmlMiddleware(server, pluginParams?.features)(req, res, next);
            });
            // Serve raw API source files without Vite transforms (for editor display)
            server.middlewares.use("/sb-raw-source/", (req, res, next) => {
                const relativePath = req.url?.replace(/^\//, "");
                if (!relativePath) {
                    return next();
                }
                try {
                    const absolutePath = path.join(config.root, relativePath);
                    // Prevent path traversal
                    if (!absolutePath.startsWith(config.root)) {
                        res.statusCode = 403;
                        res.end("Forbidden");
                        return;
                    }
                    const content = readFileSync(absolutePath, "utf-8");
                    res.setHeader("Content-Type", "text/plain; charset=utf-8");
                    res.end(content);
                }
                catch {
                    res.statusCode = 404;
                    res.end("Not found");
                }
            });
            server.middlewares.use("/sb-scopes", bodyParser.json({ strict: true }));
            server.middlewares.use("/sb-scopes", (req, res, next) => {
                void (async () => {
                    if (!isInitialized) {
                        return next();
                    }
                    if (req.method !== "GET" && req.method !== "OPTIONS") {
                        res.statusCode = 405;
                        res.end("Method not allowed");
                        return res.end();
                    }
                    res.setHeader("Access-Control-Allow-Origin", "*");
                    res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
                    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
                    if (req.method === "OPTIONS") {
                        res.statusCode = 204;
                        res.end();
                        return;
                    }
                    const appScope = await getAppScope(fileSyncManager);
                    res.setHeader("Content-Type", "application/json");
                    return res.end(JSON.stringify(appScope));
                })();
            });
            server.middlewares.use("/sb-list-files", (req, res, next) => {
                void (async () => {
                    if (!isInitialized) {
                        return next();
                    }
                    if (req.method !== "GET" && req.method !== "OPTIONS") {
                        res.statusCode = 405;
                        res.end("Method not allowed");
                        return;
                    }
                    res.setHeader("Access-Control-Allow-Origin", "*");
                    res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
                    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
                    if (req.method === "OPTIONS") {
                        res.statusCode = 204;
                        res.end();
                        return;
                    }
                    try {
                        const ignorePatterns = await createGitIgnorePatterns(config.root);
                        const files = await glob(["**/*.{ts,tsx,js,jsx,json,yaml,yml,css}"], { cwd: config.root, ignore: ignorePatterns });
                        res.setHeader("Content-Type", "application/json");
                        res.end(JSON.stringify(files.sort()));
                    }
                    catch {
                        res.statusCode = 500;
                        res.end(JSON.stringify({ error: "Failed to list files" }));
                    }
                })();
            });
            server.middlewares.use("/sb-download", bodyParser.urlencoded({ extended: false }));
            server.middlewares.use("/sb-download", (req, res, next) => {
                void (async () => {
                    if (!isInitialized) {
                        return next();
                    }
                    if (req.method !== "POST" && req.method !== "OPTIONS") {
                        res.statusCode = 405;
                        res.end("Method not allowed");
                        return;
                    }
                    res.setHeader("Access-Control-Allow-Origin", "*");
                    res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
                    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
                    if (req.method === "OPTIONS") {
                        res.statusCode = 204;
                        res.end();
                        return;
                    }
                    try {
                        const rootDir = appRootDir;
                        let metadataFilePath = null;
                        const metadata = JSON.parse(req.body.metadata);
                        const metadataDir = path.join(rootDir, ".superblocks");
                        metadataFilePath = path.join(metadataDir, "download-metadata.json");
                        writeFileSync(metadataFilePath, JSON.stringify(metadata, null, 2));
                        const filename = getDownloadArchiveFilename({
                            rootDir,
                            metadata,
                        });
                        res.setHeader("Content-Type", "application/zip");
                        res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
                        res.setHeader("Transfer-Encoding", "chunked");
                        const archive = archiver("zip", {
                            zlib: { level: 9 },
                        });
                        archive.on("error", (err) => {
                            logger.error("Archive error", getErrorMeta(err));
                        });
                        const { skip: archiveSkipPatterns, ignore: archiveIgnorePatterns } = getCodeModeDownloadArchiveGlobPatterns();
                        archive.glob("**/*", {
                            cwd: rootDir,
                            dot: true,
                            skip: archiveSkipPatterns,
                            ignore: archiveIgnorePatterns,
                        }, {});
                        void archive.finalize();
                        await pipeline(archive, res);
                        if (metadataFilePath) {
                            try {
                                unlinkSync(metadataFilePath);
                            }
                            catch (cleanupError) {
                                logger.error("Failed to cleanup metadata file", cleanupError);
                            }
                        }
                    }
                    catch (error) {
                        logger.error("Error creating zip download", error);
                        if (!res.headersSent) {
                            res.statusCode = 500;
                            res.setHeader("Content-Type", "application/json");
                            res.end(JSON.stringify({ error: "Failed to create download" }));
                        }
                    }
                })();
            });
            server.middlewares.use("/sb-set-file-contents", bodyParser.json({ strict: true }));
            server.middlewares.use("/sb-set-file-contents", (req, res, next) => {
                void (async () => {
                    if (!isInitialized) {
                        return next();
                    }
                    if (req.method !== "POST" && req.method !== "OPTIONS") {
                        res.statusCode = 405;
                        res.end("Method not allowed");
                        return;
                    }
                    res.setHeader("Access-Control-Allow-Origin", "*");
                    res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
                    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
                    if (req.method === "OPTIONS") {
                        res.statusCode = 204;
                        res.end();
                        return;
                    }
                    try {
                        const body = req.body;
                        // Validate request body
                        if (!body ||
                            typeof body.path !== "string" ||
                            typeof body.contents !== "string") {
                            res.statusCode = 400;
                            res.setHeader("Content-Type", "application/json");
                            res.end(JSON.stringify({
                                error: "Invalid request body. Expected { path: string, contents: string }",
                            }));
                            return;
                        }
                        await fileSyncManager.setFileContents(body.path, body.contents);
                        res.setHeader("Content-Type", "application/json");
                        res.end(JSON.stringify({ success: true }));
                    }
                    catch (error) {
                        logger.error("Error setting file contents", getErrorMeta(error));
                        if (!res.headersSent) {
                            res.statusCode = 500;
                            res.setHeader("Content-Type", "application/json");
                            res.end(JSON.stringify({
                                error: error instanceof Error
                                    ? error.message
                                    : "Failed to set file contents",
                            }));
                        }
                    }
                })();
            });
            socketManager = createSocketManager(pluginParams.tracer);
            registerSnapshotRoutes(server, {
                getIsInitialized: () => isInitialized,
                appRootDir,
                snapshotManager: pluginParams.snapshotManager,
                socketManager,
                logger,
                getAvailableIntegrationIds: aiService
                    ? () => aiService.getAvailableIntegrationIds()
                    : undefined,
                isSessionManagementEnabled: aiService?.isSessionManagementEnabled ?? false,
            });
            registerMigrationRoutes(server, {
                appRootDir,
                getIsInitialized: () => isInitialized,
                syncService,
                logger,
                viteRoot: config.root,
            });
            await tracer.startActiveSpan("socketServerStart", async (socketSpan) => {
                try {
                    socketManager.startSocketServer({
                        server: httpServer,
                        fileSyncManager,
                        lockService,
                        syncService,
                        aiService,
                        gitService,
                        gitBootstrapFailed: pluginParams.gitBootstrapFailed,
                        activateGitService,
                        checkAuthorization: pluginParams.checkAuthorization,
                        tracer: pluginParams.tracer,
                        path: "/sb-updates",
                        gitOperationLock,
                        superblocksBaseUrl: pluginParams.superblocksBaseUrl,
                    });
                }
                finally {
                    socketSpan.end();
                }
            });
            socketManager.removeAllListeners();
            const sendError = (errorEvent) => {
                logger.error("Unhandled server error", getErrorMeta(errorEvent));
                if (aiService?.isBusy()) {
                    return;
                }
                void socketManager.callEditorClients((socket) => socket.call.error(errorEvent));
            };
            const isExpectedRejection = (reason) => {
                const code = reason instanceof Error
                    ? reason.code
                    : typeof reason === "object" && reason !== null
                        ? reason.code
                        : undefined;
                // code 8 = connection closed, code 10 = post-close request
                // These are expected when users close browser tabs or navigate away
                return code === 8 || code === 10;
            };
            // Add some process error handling to ensure to notify the user of
            // any uncaught exceptions or rejections
            process.on("uncaughtException", (error) => {
                sendError(makeServerError(error));
            });
            process.on("unhandledRejection", (reason) => {
                if (isExpectedRejection(reason)) {
                    logger.warn(`Expected unhandled rejection: ${JSON.stringify(reason)}`);
                    return;
                }
                const reasonStr = JSON.stringify(reason);
                logger.error(`Unhandled rejection: ${reasonStr}`);
                const error = reason instanceof Error
                    ? reason
                    : typeof reason === "object"
                        ? new Error(JSON.stringify(reason))
                        : new Error(String(reason));
                sendError(makeServerError(error));
            });
            syncService?.removeListener?.("error", sendError);
            syncService?.on?.("error", sendError);
            lockService?.removeListener?.("error", sendError);
            lockService?.on?.("error", sendError);
            syncService?.on?.("degradedModeChange", (degradedMode) => {
                void (async () => {
                    void socketManager.callEditorClients((socket) => {
                        return socket.call.setDegradedMode({ degradedMode });
                    });
                })();
            });
            // Forward Clark logs to browser DevTools
            if (aiService) {
                unregisterLogForwarder = createClarkLogForwarder({
                    getEditorClients: () => socketManager.getEditorClients(),
                    getClarkState: () => aiService.clarkState,
                });
            }
            const sendFileUpdate = async (_path) => {
                const appScope = await getAppScope(fileSyncManager);
                void socketManager.callEditorClients((socket) => socket.call.resetScope({ appScope }));
            };
            const toApiV3Dto = (api) => {
                const { apiPb } = api;
                return {
                    id: apiPb.metadata.id,
                    name: apiPb.metadata.name,
                    scopeId: apiPb.trigger?.application?.id,
                    applicationId: apiPb.trigger?.application?.id,
                    pageId: apiPb.trigger?.application?.pageId,
                    organizationId: apiPb.metadata?.organization,
                    updated: apiPb.metadata?.timestamps?.updated
                        ? new Date(apiPb.metadata?.timestamps?.updated)
                        : undefined,
                    triggerType: ApiTriggerType.UI,
                    apiPb: apiPb,
                };
            };
            // configured twice otherwise, always remove all listeners
            fileSyncManager.removeAllListeners();
            fileSyncManager.on("error", sendError);
            fileSyncManager.on("routerChange", (routerFilePath) => {
                void (async () => {
                    try {
                        logger.info("Router file changed, re-parsing routes");
                        const routes = await parseRouterFile(routerFilePath, config.root);
                        logger.info(`Routes updated: ${routes.length} routes found`);
                        // Update editor clients with new app scope
                        void sendFileUpdate(routerFilePath);
                        await syncService?.uploadDirectory(aiService?.isBusy() ? "ai:generate:interim" : "fileChanged");
                    }
                    catch (e) {
                        logger.error("Error parsing router file", getErrorMeta(e));
                    }
                })();
            });
            if (aiService) {
                fileSyncManager.on("fileChanged", debounce((filePath) => {
                    void (async () => {
                        const appScope = await getAppScope(fileSyncManager);
                        void aiService.appContext.handleAppScopeReset(appScope);
                        if (!aiService.isBusy()) {
                            void aiService.handleUserChange({
                                type: "fileChanged",
                                filePath,
                            });
                        }
                    })();
                }, 1000));
                fileSyncManager.on("addPage", (path) => {
                    void (async () => {
                        const appScope = await getAppScope(fileSyncManager);
                        void aiService.appContext.handleAppScopeReset(appScope);
                        if (!aiService.isBusy()) {
                            void aiService.handleUserChange({ type: "addPage", path });
                        }
                    })();
                });
                fileSyncManager.on("deletePage", (path) => {
                    void (async () => {
                        const appScope = await getAppScope(fileSyncManager);
                        void aiService.appContext.handleAppScopeReset(appScope);
                        if (!aiService.isBusy()) {
                            void aiService.handleUserChange({ type: "deletePage", path });
                        }
                    })();
                });
            }
            const debouncedFileUpdate = (filePath) => {
                let fn = debouncedFileUpdateHandlers.get(filePath);
                if (!fn) {
                    fn = debounce(() => {
                        void sendFileUpdate(filePath);
                    }, 100, { leading: true, trailing: true, maxWait: 500 });
                }
                debouncedFileUpdateHandlers.set(filePath, fn);
                fn();
            };
            fileSyncManager.on("fileChanged", (path, _content) => {
                void (async () => {
                    void debouncedFileUpdate(path);
                    if (syncService) {
                        // Use different source during AI generation to prevent individual draft commits
                        const source = aiService && aiService.isBusy()
                            ? "ai:generate:interim"
                            : "fileChanged";
                        // Eagerly sync server file changes so ensureFilesSynced() is a
                        // no-op on the SDK API execution path (avoids 10s debounce wait).
                        // Uses `source` so AI-generation edits sync without creating commits.
                        // Debounced at 750ms to coalesce Clark's burst writes (e.g. API
                        // file + index.ts barrel, typically 1-4s apart) into one upload.
                        // Scheduled before the `uploadDirectory` await so the most-recent
                        // event's `source` wins even if prior IIFEs complete out of order.
                        if (normalizePath(path).startsWith(serverPrefix)) {
                            clearTimeout(eagerServerSyncTimer);
                            eagerServerSyncTimer = setTimeout(() => {
                                syncService.uploadDirectoryNowIfNeeded(source).catch((err) => {
                                    logger.error("Eager server file sync failed", getErrorMeta(err));
                                });
                            }, 750);
                        }
                        await syncService.uploadDirectory(source);
                    }
                })();
            });
            fileSyncManager.on("apiUpdate", (payload) => {
                void (async () => {
                    void socketManager.callEditorClients(async (socket) => {
                        if (payload.api.apiPb?.metadata?.id) {
                            const inputs = await extractInputsFromApi(payload.api);
                            void socket.call
                                .updateApiInputs({
                                apiId: payload.api.apiPb.metadata.id,
                                inputs,
                            })
                                .catch(() => {
                                logger.warn(`Failed to update API inputs on apiUpdate event for ${payload.api.apiPb.metadata.id} (connection closed)`);
                            });
                        }
                        return socket.call.manualUpdateApi({
                            api: {
                                ...payload.api,
                                scopeId: payload.scopeId ?? "",
                            },
                        });
                    });
                    if (syncService) {
                        await syncService.uploadDirectory("apiUpdate");
                    }
                    if (aiService) {
                        void aiService.handleApiUpdate({
                            author: "clark",
                            payload: toApiV3Dto(payload.api),
                        });
                    }
                })();
            });
            fileSyncManager.on("apiManualUpdate", (payload, sendScopeUpdates = false) => {
                void (async () => {
                    if (sendScopeUpdates) {
                        void sendFileUpdate(path.join(config.root, "App.tsx"));
                    }
                    if (syncService) {
                        // Use different source during AI generation to prevent individual draft commits
                        const source = aiService && aiService.isBusy()
                            ? "ai:generate:interim"
                            : "apiManualUpdate";
                        void syncService.uploadDirectory(source);
                    }
                    void socketManager.callEditorClients(async (socket) => {
                        const inputs = await extractInputsFromApi(payload.api);
                        void socket.call
                            .updateApiInputs({
                            apiId: payload.api.apiPb.metadata.id,
                            inputs,
                        })
                            .catch(() => {
                            logger.warn(`Failed to update API inputs on file change for ${payload.api.apiPb.metadata.id} (connection closed)`);
                        });
                        return socket.call.manualUpdateApi({
                            api: {
                                ...payload.api,
                                scopeId: payload.scopeId ?? "",
                            },
                        });
                    });
                    if (aiService) {
                        void aiService.handleApiUpdate({
                            author: "user",
                            payload: toApiV3Dto(payload.api),
                        });
                    }
                })();
            });
            if (syncService) {
                syncService.on("synchronizationStatusChange", (status, generationNumber) => {
                    void (async () => {
                        void socketManager.callEditorClients(async (socket) => {
                            try {
                                await socket.call.syncStatus({ status, generationNumber });
                            }
                            catch (err) {
                                // Failed to send is expected when clients disconnect - log as WARNING
                                logger.warn("Failed to send syncStatus", getErrorMeta(err));
                            }
                        });
                    })();
                });
            }
            if (lockService) {
                lockService.on("statusChange", (status) => {
                    if (status === LockServiceStatus.RELEASING ||
                        status === LockServiceStatus.LOCK_INVALID) {
                        void socketManager.callEditorClients(async (socket) => {
                            try {
                                await socket.call.releasingLock();
                            }
                            catch (err) {
                                // Failed to send is expected when clients disconnect - log as WARNING
                                logger.warn("Failed to send releasingLock", getErrorMeta(err));
                            }
                        });
                    }
                });
                lockService.on("closedByServer", (data) => {
                    void socketManager.callEditorClients(async (socket) => {
                        try {
                            await socket.call.closedByServer(data);
                        }
                        catch (err) {
                            // Failed to send is expected when clients disconnect - log as WARNING
                            logger.warn("Failed to send closedByServer", getErrorMeta(err));
                        }
                    });
                });
            }
            fileSyncManager.on("apiDelete", (payload) => {
                void (async () => {
                    if (syncService) {
                        await syncService.uploadDirectory("apiDelete");
                    }
                    if (aiService) {
                        for (const api of payload.apis) {
                            void aiService.appContext.handleApiDelete(api.apiName);
                            if (!aiService.isBusy()) {
                                void aiService.handleUserChange({
                                    type: "apiDelete",
                                    apiName: api.apiName,
                                });
                            }
                        }
                    }
                })();
            });
            fileSyncManager.on("apiManualDelete", (payload) => {
                void (async () => {
                    void socketManager.callEditorClients((socket) => {
                        return socket.call.manualDeleteApi({
                            id: payload.api.id,
                        });
                    });
                    if (syncService) {
                        // Use different source during AI generation to prevent individual draft commits
                        const source = aiService && aiService.isBusy()
                            ? "ai:generate:interim"
                            : "apiManualDelete";
                        await syncService.uploadDirectory(source);
                    }
                    if (aiService) {
                        void aiService.appContext.handleApiDelete(payload.api.apiName);
                        if (!aiService.isBusy()) {
                            void aiService.handleUserChange({
                                type: "apiDelete",
                                apiName: payload.api.apiName,
                            });
                        }
                    }
                })();
            });
            fileSyncManager.on("appFactsChanged", () => {
                void socketManager.callEditorClients((socket) => {
                    return socket.call.appFactsChanged();
                });
            });
            if (syncService && aiService) {
                socketManager.on("connect:editor", (socket, peerId) => {
                    void (async () => {
                        if (gitService) {
                            // Advance local superblocks/live to match origin (branch pointer + index only;
                            // working tree stays DBFS-managed).
                            try {
                                await gitService.fetch("origin", SUPERBLOCKS_LIVE_GIT_BRANCH);
                                await gitService.raw([
                                    "reset",
                                    `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`,
                                ]);
                            }
                            catch (error) {
                                getLogger().warn("[git] runtime sync failed on editor connect, sidebar may show stale history", {
                                    applicationId: syncService.applicationId,
                                    branchName: SUPERBLOCKS_LIVE_GIT_BRANCH,
                                    error: error instanceof Error ? error.message : String(error),
                                });
                            }
                            const didRefresh = await syncService.refreshIfStale();
                            if (didRefresh) {
                                logger.info("[file-sync] invalidated chat cache after stale DBFS refresh", {
                                    applicationId: syncService.applicationId,
                                    branchName: syncService.branchName,
                                });
                                aiService.chatSessionStore.invalidateCache();
                            }
                        }
                        // Rebind StablePeer's inner on every connect so queued calls flush.
                        aiService.handleUserConnected(socket, peerId);
                        const allApis = fileSyncManager.getApiFiles();
                        for (const api of Object.values(allApis)) {
                            if (api.api.apiPb?.metadata?.id) {
                                const inputs = await extractInputsFromApi(api.api);
                                void socket.call
                                    .updateApiInputs({
                                    apiId: api.api.apiPb.metadata.id,
                                    inputs,
                                })
                                    .catch(() => {
                                    logger.warn(`Failed to update API inputs on client connection for ${api.api.apiPb.metadata.id} (connection closed)`);
                                });
                            }
                        }
                        // Send any pending file transform error
                        if (pendingFileTransformError) {
                            const { filePath, error } = pendingFileTransformError;
                            const fileTransformError = makeServerError(error, {
                                critical: false,
                            });
                            fileTransformError.message = `File transformation error in ${filePath.replace(/^.*\/pages\//, "pages/")}: ${error.message}`;
                            await socket.call.error(fileTransformError);
                            pendingFileTransformError = null;
                        }
                        const hasDraft = await syncService.hasLocalDraftChanges();
                        const appScope = await getAppScope(fileSyncManager);
                        await socket.call.resetScope({ appScope });
                        return socket.call.aiSetDraftState({ hasDraft });
                    })();
                });
                socketManager.on("disconnect:editor", (peerId) => {
                    aiService.handleUserDisconnected(peerId);
                });
                aiService.setOnGitOperationComplete(() => {
                    void socketManager.callEditorClients((socket) => socket.call.requestGitRefresh());
                });
                syncService.on("draftStateChanged", (hasDraft) => {
                    logger.info("[file-sync] draftStateChanged event", { hasDraft });
                    if (hasDraft) {
                        hasHadAnyDrafts = true;
                        logger.debug("hasHadAnyDrafts set to true");
                    }
                    void (async () => {
                        // Notify UI clients
                        void socketManager.callEditorClients((socket) => {
                            return socket.call.aiSetDraftState({ hasDraft });
                        });
                        // Notify server
                        await syncService.reportDraftStateToServer(hasDraft);
                    })();
                });
                aiService.on("generationStarted", () => {
                    void (async () => {
                        await syncService.uploadDirectory("ai:generate:pre");
                        await syncService.uploadDirectoryNowIfNeeded("ai:generate:pre");
                    })();
                });
                aiService.on("generationCompleted", (hasDraft) => {
                    void (async () => {
                        logger.info("[file-sync] generationCompleted event received", {
                            applicationId: syncService.applicationId,
                            branchName: syncService.branchName,
                            hasDraft,
                            hasHadAnyDrafts,
                            isFirstContentfulGeneration,
                        });
                        aiService.handleClearBuildSystemErrors();
                        // Always fire full-reload after the first generation
                        if (isFirstContentfulGeneration && hasHadAnyDrafts) {
                            logger.debug("Sending full-reload after first generation");
                            server.ws.send({
                                type: "full-reload",
                            });
                            isFirstContentfulGeneration = false;
                        }
                        // Upload directory after successful AI generation
                        if (hasDraft) {
                            if (!aiService.hasConnectedPeer()) {
                                logger.warn("[file-sync] hasDraft=true but no peer is connected");
                            }
                            logger.info("[file-sync] Triggering post-generation upload");
                            await syncService.uploadDirectory("ai:generate:post");
                            const result = await syncService.uploadDirectoryNowIfNeeded("ai:generate:post");
                            logger.info("[file-sync] Post-generation upload completed", {
                                applicationId: syncService.applicationId,
                                branchName: syncService.branchName,
                                commitId: result.commitId,
                                directoryHashPrefix: result.directoryHash.slice(0, 12),
                            });
                        }
                        else {
                            logger.warn("[file-sync] Skipping post-generation upload because hasDraft=false", {
                                applicationId: syncService.applicationId,
                                branchName: syncService.branchName,
                                hasHadAnyDrafts,
                                isFirstContentfulGeneration,
                            });
                        }
                    })();
                });
                aiService.on("generationFailed", () => {
                    logger.error("Generation failed");
                });
                const appScope = await getAppScope(fileSyncManager);
                void (async () => {
                    await aiService.appContext.handleAppScopeReset(appScope);
                })();
            }
            const triggerRegisteredComponentUpdate = () => {
                const module = server.moduleGraph.getModuleById(resolvedVirtualRegisteredComponentsId);
                if (module) {
                    server.moduleGraph.invalidateModule(module, new Set(), Date.now(), true);
                    void server.reloadModule(module);
                }
            };
            componentsManager.on("error", sendError);
            componentsManager.on("registeredComponentAdded", triggerRegisteredComponentUpdate);
            componentsManager.on("registeredComponentRemoved", (name) => {
                triggerRegisteredComponentUpdate();
                void socketManager.callLibraryClients((socket) => {
                    return socket.call.removeRegisteredComponent({
                        name,
                    });
                });
            });
            componentsManager.on("registeredComponentChanged", (name, oldName) => {
                void (async () => {
                    triggerRegisteredComponentUpdate();
                    if (oldName) {
                        void socketManager.callLibraryClients(async (socket) => {
                            await socket.call.renameRegisteredComponent({
                                name,
                                oldName,
                            });
                        });
                    }
                })();
            });
        },
        async transform(code, id) {
            if (id.includes("/node_modules/") || id.endsWith(".d.ts")) {
                return;
            }
            const relativePath = path.relative(config.root, id);
            if (!this.meta.watchMode)
                return;
            if (id.endsWith(".tsx")) {
                try {
                    const normalizedId = normalizePath(id);
                    if (!fileSyncManager.readFile(normalizedId)) {
                        return { code, map: null };
                    }
                    const fileContents = await fileSyncManager.getFileWithIds(normalizedId);
                    if (!fileContents) {
                        throw new Error("File not found in file sync manager " + id);
                    }
                    if (id === fileSyncManager.routerFilePath) {
                        return;
                    }
                    if (componentsManager.isCustomComponentFile(normalizedId)) {
                        return {
                            code: fileContents,
                            map: null,
                        };
                    }
                    const isComponentFile = id.endsWith(".tsx") ||
                        id.endsWith(".jsx") ||
                        id.endsWith(".ts") ||
                        id.endsWith(".js");
                    if (isComponentFile && componentsManager.isInComponentDirectory(id)) {
                        return {
                            code: fileContents,
                            map: null,
                        };
                    }
                    if (isPageFilePath(id)) {
                        // Clear pending transform error for this file since transform succeeded
                        if (pendingFileTransformError?.filePath === relativePath &&
                            aiService) {
                            pendingFileTransformError = null;
                            aiService.handleClearBuildSystemErrors();
                        }
                        return {
                            code: `import "${virtualRegisteredComponentsId}";\n\n${fileContents}`,
                            map: null,
                        };
                    }
                    // Clear pending transform error for this file since transform succeeded
                    if (pendingFileTransformError?.filePath === relativePath &&
                        aiService) {
                        pendingFileTransformError = null;
                        aiService.handleClearBuildSystemErrors();
                    }
                    return {
                        code: fileContents,
                        map: null,
                    };
                }
                catch (e) {
                    if (aiService) {
                        const error = e instanceof Error ? e : new Error(String(e));
                        getLogger().info("Sending transform error to UI for file: " + relativePath);
                        addFileTransformError(relativePath, error);
                        // Add transform error as build system error
                        aiService.handleBuildSystemError(error);
                    }
                    logger.error("Error transforming file", getErrorMeta(e));
                }
            }
            else if (id.endsWith(SCOPE_FILE)) {
                const normalizedId = normalizePath(id);
                const fileContents = await fileSyncManager.getFileWithIds(normalizedId);
                if (!fileContents) {
                    throw new Error("File not found in file sync manager " + id);
                }
                return {
                    code: fileContents,
                    map: null,
                };
            }
            return {
                code,
                map: null,
            };
        },
        resolveId(id) {
            if (id === virtualLegacyId && isCustomBuildEnabled()) {
                return resolvedVirtualLegacyId;
            }
            if (id === virtualRegisteredComponentsId ||
                id === `/${virtualRegisteredComponentsId}`) {
                return resolvedVirtualRegisteredComponentsId;
            }
        },
        async load(id) {
            // This is a virtual file that we use to wrap any components that are using a
            // version of React that is not our current library version. Those components need to be
            // mounted with the version of React that is being used inside the "legacy" folder.
            if (id === resolvedVirtualLegacyId) {
                const customFolder = getLegacyComponentsFolder();
                return /*js*/ `
          import React from "react";
          import ReactDOMLegacy from "/${customFolder}/dist/react-dom";
          import ReactLegacy from "/${customFolder}/dist/react";

          export function wrapCustomComponent(CustomComponent) {
            return React.forwardRef((props, ref) => {
              const containerRef = React.useRef(null);

              React.useEffect(() => {
                if (!containerRef.current) return;

                const container = containerRef.current;

                ReactDOMLegacy.render(
                  ReactLegacy.createElement(CustomComponent, props),
                  container,
                );

                return () => {
                  if (container) {
                    ReactDOMLegacy.unmountComponentAtNode(container);
                  }
                };
              }, [props]);

              return React.createElement("div", { ref: mergeRefs(containerRef, ref) });
            });
          }

          function mergeRefs(...refs) {
            return (value) => {
              refs.forEach((ref) => {
                if (typeof ref === "function") {
                  ref(value);
                } else if (ref != null) {
                  ref.current = value;
                }
              });
            };
          }
        `;
            }
            if (id === resolvedVirtualRegisteredComponentsId) {
                const paths = componentsManager.componentFilePaths;
                return /*js*/ `
          ${Array.from(paths)
                    .map((filePath) => {
                    // Normalize to forward slashes for import paths (cross-platform compatibility)
                    const relativePath = normalizePath(path.relative(config.root, filePath));
                    return `
                import "/${relativePath}";
              `;
                })
                    .join("\n")}
          `;
            }
        },
        async handleHotUpdate(ctx) {
            // Prevent full page reloads for API files - they're handled by the file system watcher
            if (isApiFilePath(ctx.file)) {
                return [];
            }
            // Prevent full page reloads for skill files (app facts) - they're internal metadata
            if (isSkillFilePath(ctx.file)) {
                return [];
            }
            // if this HMR call reloads a file that an error was thrown about before,
            // perform a full-reload and clear the error.
            if (loggedViteErrors.length > 0) {
                // Normalize paths for comparison only (Windows compatibility)
                const normalizedCtxFile = normalizePath(ctx.file);
                const errorIndex = loggedViteErrors.findIndex((error) => {
                    return ("id" in error &&
                        error.id &&
                        normalizePath(error.id) === normalizedCtxFile);
                });
                if (errorIndex !== -1) {
                    loggedViteErrors.splice(errorIndex, 1);
                    logger.debug("Sending full-reload after error");
                    void ctx.server.ws.send({ type: "full-reload" });
                    return [];
                }
            }
            if (ctx.file.startsWith(config.root)) {
                if (aiService?.isBusy()) {
                    // let's check to make sure the file being written has the correct imports
                    // there is no real nice way to accomplish this server side, because we need the browser to resolve all of a
                    // file's exports, which we cannot do in any sane way via vite. when an export cannot be resolved, the browser
                    // will throw an SyntaxError, and we can catch that and send it back here to the server.
                    // it's janky, but it works. this does mean we incur a slight performance hit, because in the happy path we are
                    // going to do two import calls (this one, and the one normal vite HMR does), but it's a small price to pay for a
                    // better gen experience.
                    try {
                        const libraryClient = socketManager.getAnyLibraryClient();
                        if (!libraryClient) {
                            return;
                        }
                        const module = ctx.server.moduleGraph.getModuleById(ctx.file);
                        if (module) {
                            const error = await libraryClient.call.clientImport({
                                importPath: module.url,
                            });
                            if (error) {
                                const err = new Error(error.message);
                                err.stack = error.stack;
                                err.name = error.name;
                                void aiService.handleBuildSystemError(err);
                                logger.error(`Error loading import ${module.url}: ${error.message}`, getErrorMeta(err));
                                return [];
                            }
                        }
                    }
                    catch {
                        // we ignore these and just fall through to the normal import checking
                    }
                }
                const source = await ctx.read();
                const importsToCheck = getImports(source);
                for (const importPath of importsToCheck) {
                    try {
                        let finalPath = await getFullImportPath(importPath);
                        // super simple check to see if we are trying a relative import
                        // we don't want to catch absolute imports (node modules, root imports)
                        // because the client environment is resolving from the root of the app
                        if (!finalPath &&
                            (importPath.startsWith("./") || importPath.startsWith("../"))) {
                            finalPath = path.join(path.dirname(ctx.file), importPath);
                        }
                        // important! we do this here intentionally instead of a ?? chain above because the resolver above,
                        // should have already resolved the import path, but if it didn't we want to first check if we can do the relative path check
                        // then do a final fallback to the original import path
                        else if (!finalPath) {
                            finalPath = importPath;
                        }
                        await ctx.server.environments?.client?.fetchModule(finalPath, ctx.file);
                    }
                    catch (error) {
                        if (error instanceof Error &&
                            "code" in error &&
                            (error.code === "ERR_LOAD_URL" ||
                                error.code === "ERR_MODULE_NOT_FOUND")) {
                            // we don't need to send the same error multiple times
                            if (lastImportErrorFiles.has(ctx.file)) {
                                return [];
                            }
                            lastImportErrorFiles.add(ctx.file);
                            if (aiService?.isBusy()) {
                                void aiService.handleBuildSystemError(error);
                                return [];
                            }
                            else {
                                // Rethrow as ImportError so Vite's error overlay can handle it
                                // This will trigger vite:error events that the vanilla overlay listens to
                                throw new ImportError(error.message, importPath);
                            }
                        }
                    }
                }
            }
            // Clear import errors when file is successfully transformed
            if (lastImportErrorFiles.has(ctx.file)) {
                lastImportErrorFiles.delete(ctx.file);
                // Clear build system errors when import errors are resolved
                if (aiService?.isBusy()) {
                    aiService.handleClearBuildSystemErrors();
                }
            }
            // this is a bandaid fix for a full-reload problem to ensure that we do not trigger full page reloads
            // for components that are not imported anywhere. please leave it alone for now, or talk to omar if
            // this is causing problems and you need to fix it.
            return ctx.modules.filter((module) => {
                if (module.id?.startsWith(ctx.file)) {
                    return module.importers.size > 0;
                }
                return true;
            });
        },
    };
    return {
        plugin: [plugin, yamlApiTypesPlugin(syncService, aiService)],
        dispose: () => {
            // Cancel all pending debounced root updates
            debouncedFileUpdateHandlers.forEach((fn) => fn.cancel());
            debouncedFileUpdateHandlers.clear();
            unregisterLogForwarder?.();
            disposeSocketManager();
        },
    };
}
async function getAppScope(fileSyncManager) {
    const pageContents = await getAllPages({
        rootDir: config.root,
    });
    const pages = {};
    for (const { name, route, fileName } of pageContents) {
        const roots = await fileSyncManager.getPageRootNodes(fileName);
        const pageScopeDef = fileSyncManager.getScope(fileName);
        if (!roots || !pageScopeDef)
            continue;
        if (pages[name]) {
            // Page already exists, add this route to its routes array
            pages[name].routes.push(route);
            continue;
        }
        // New page, initialize with routes array
        pages[name] = {
            name,
            ...pageScopeDef,
            routes: [route],
            roots,
        };
    }
    const appScopeDef = fileSyncManager.getScope(fileSyncManager.appFilePath, true);
    const appRoots = await fileSyncManager.getAppRootNodes();
    const apiFiles = fileSyncManager.getApiFiles();
    const apis = Object.values(apiFiles)
        .filter(({ api: { apiPb } }) => !!apiPb.metadata)
        .map(({ api: { apiPb }, scopeId }) => ({
        id: apiPb.metadata.id,
        name: apiPb.metadata.name,
        applicationId: apiPb.trigger?.application?.id,
        pageId: apiPb.trigger?.application?.pageId,
        organizationId: apiPb.metadata.organization,
        updated: apiPb.metadata?.timestamps?.updated
            ? new Date(apiPb.metadata?.timestamps?.updated)
            : undefined,
        triggerType: ApiTriggerType.UI,
        apiPb: apiPb,
        scopeId,
    }));
    const appScope = {
        ...appScopeDef,
        roots: appRoots ?? [],
        pages,
        apis,
    };
    return appScope;
}
//# sourceMappingURL=file-sync-vite-plugin.js.map