import express, { Router } from "express";
import path from "node:path";
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import { httpLogger, errorHandler } from "./middleware/index.js";
import { actorMiddleware } from "./middleware/auth.js";
import { boardMutationGuard } from "./middleware/board-mutation-guard.js";
import { privateHostnameGuard, resolvePrivateHostnameAllowSet } from "./middleware/private-hostname-guard.js";
import { healthRoutes } from "./routes/health.js";
import { companyRoutes } from "./routes/companies.js";
import { companySkillRoutes } from "./routes/company-skills.js";
import { teamsCatalogRoutes } from "./routes/teams-catalog.js";
import { agentRoutes } from "./routes/agents.js";
import { projectRoutes } from "./routes/projects.js";
import { issueRoutes } from "./routes/issues.js";
import { issueTreeControlRoutes } from "./routes/issue-tree-control.js";
import { routineRoutes } from "./routes/routines.js";
import { environmentRoutes } from "./routes/environments.js";
import { executionWorkspaceRoutes } from "./routes/execution-workspaces.js";
import { goalRoutes } from "./routes/goals.js";
import { approvalRoutes } from "./routes/approvals.js";
import { secretRoutes } from "./routes/secrets.js";
import { costRoutes } from "./routes/costs.js";
import { activityRoutes } from "./routes/activity.js";
import { dashboardRoutes } from "./routes/dashboard.js";
import { userProfileRoutes } from "./routes/user-profiles.js";
import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js";
import { sidebarPreferenceRoutes } from "./routes/sidebar-preferences.js";
import { resourceMembershipRoutes } from "./routes/resource-memberships.js";
import { inboxDismissalRoutes } from "./routes/inbox-dismissals.js";
import { instanceSettingsRoutes } from "./routes/instance-settings.js";
import { openApiRoutes } from "./routes/openapi.js";
import { instanceDatabaseBackupRoutes, } from "./routes/instance-database-backups.js";
import { llmRoutes } from "./routes/llms.js";
import { authRoutes } from "./routes/auth.js";
import { assetRoutes } from "./routes/assets.js";
import { accessRoutes } from "./routes/access.js";
import { pluginRoutes } from "./routes/plugins.js";
import { adapterRoutes } from "./routes/adapters.js";
import { pluginUiStaticRoutes } from "./routes/plugin-ui-static.js";
import { readBrandedStaticIndexHtml } from "./static-index-html.js";
import { applyUiBranding } from "./ui-branding.js";
import { logger } from "./middleware/logger.js";
import { DEFAULT_LOCAL_PLUGIN_DIR, pluginLoader } from "./services/plugin-loader.js";
import { createPluginWorkerManager } from "./services/plugin-worker-manager.js";
import { createPluginJobScheduler } from "./services/plugin-job-scheduler.js";
import { pluginJobStore } from "./services/plugin-job-store.js";
import { createPluginToolDispatcher } from "./services/plugin-tool-dispatcher.js";
import { pluginLifecycleManager } from "./services/plugin-lifecycle.js";
import { createPluginJobCoordinator } from "./services/plugin-job-coordinator.js";
import { buildHostServices, flushPluginLogBuffer } from "./services/plugin-host-services.js";
import { createPluginEventBus } from "./services/plugin-event-bus.js";
import { setPluginEventBus } from "./services/activity-log.js";
import { createPluginDevWatcher } from "./services/plugin-dev-watcher.js";
import { createPluginHostServiceCleanup } from "./services/plugin-host-service-cleanup.js";
import { pluginRegistryService } from "./services/plugin-registry.js";
import { createHostClientHandlers } from "@paperclipai/plugin-sdk";
import { createCachedViteHtmlRenderer } from "./vite-html-renderer.js";
import { DEFAULT_JSON_BODY_LIMIT, PORTABLE_JSON_BODY_LIMIT } from "./http/body-limits.js";
import { COMPANY_IMPORT_API_PATH } from "./routes/company-import-paths.js";
const FEEDBACK_EXPORT_FLUSH_INTERVAL_MS = 5_000;
const VITE_DEV_ASSET_PREFIXES = [
    "/@fs/",
    "/@id/",
    "/@react-refresh",
    "/@vite/",
    "/assets/",
    "/node_modules/",
    "/src/",
];
const VITE_DEV_STATIC_PATHS = new Set([
    "/apple-touch-icon.png",
    "/favicon-16x16.png",
    "/favicon-32x32.png",
    "/favicon.ico",
    "/favicon.svg",
    "/site.webmanifest",
    "/sw.js",
]);
export function isDatabaseConnectionUnavailableError(err) {
    const error = err;
    if (error?.code === "ECONNREFUSED")
        return true;
    return Boolean(error?.cause && isDatabaseConnectionUnavailableError(error.cause));
}
export function resolveViteHmrPort(serverPort) {
    if (serverPort <= 55_535) {
        return serverPort + 10_000;
    }
    return Math.max(1_024, serverPort - 10_000);
}
export function resolveViteHmrHost(bindHost) {
    const normalized = bindHost.trim().toLowerCase();
    if (normalized === "0.0.0.0" || normalized === "::")
        return undefined;
    return bindHost;
}
export function shouldServeViteDevHtml(req) {
    const pathname = req.path;
    if (VITE_DEV_STATIC_PATHS.has(pathname))
        return false;
    if (VITE_DEV_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix)))
        return false;
    return req.accepts(["html"]) === "html";
}
export function shouldEnablePrivateHostnameGuard(opts) {
    return (opts.deploymentExposure === "private" &&
        (opts.deploymentMode === "local_trusted" || opts.deploymentMode === "authenticated"));
}
export async function createApp(db, opts) {
    const app = express();
    const captureRawBody = (req, _res, buf) => {
        req.rawBody = buf;
    };
    app.use(COMPANY_IMPORT_API_PATH, express.json({
        limit: PORTABLE_JSON_BODY_LIMIT,
        verify: captureRawBody,
    }));
    app.use(express.json({
        limit: DEFAULT_JSON_BODY_LIMIT,
        verify: captureRawBody,
    }));
    app.use(httpLogger);
    const privateHostnameGateEnabled = shouldEnablePrivateHostnameGuard({
        deploymentMode: opts.deploymentMode,
        deploymentExposure: opts.deploymentExposure,
    });
    const privateHostnameAllowSet = resolvePrivateHostnameAllowSet({
        allowedHostnames: opts.allowedHostnames,
        bindHost: opts.bindHost,
    });
    app.use(privateHostnameGuard({
        enabled: privateHostnameGateEnabled,
        allowedHostnames: opts.allowedHostnames,
        bindHost: opts.bindHost,
    }));
    app.use(actorMiddleware(db, {
        deploymentMode: opts.deploymentMode,
        resolveSession: opts.resolveSession,
    }));
    app.use("/api/auth", authRoutes(db));
    if (opts.betterAuthHandler) {
        app.all("/api/auth/{*authPath}", opts.betterAuthHandler);
    }
    app.use(llmRoutes(db));
    const hostServicesDisposers = new Map();
    const workerManager = opts.pluginWorkerManager ?? createPluginWorkerManager();
    // Mount API routes
    const api = Router();
    api.use(boardMutationGuard());
    api.use("/health", healthRoutes(db, {
        deploymentMode: opts.deploymentMode,
        deploymentExposure: opts.deploymentExposure,
        authReady: opts.authReady,
        companyDeletionEnabled: opts.companyDeletionEnabled,
    }));
    api.use(openApiRoutes());
    api.use("/companies", companyRoutes(db, opts.storageService));
    api.use(llmRoutes(db));
    api.use(companySkillRoutes(db));
    api.use(teamsCatalogRoutes(db));
    api.use(agentRoutes(db, { pluginWorkerManager: workerManager }));
    api.use(assetRoutes(db, opts.storageService));
    api.use(projectRoutes(db));
    api.use(issueRoutes(db, opts.storageService, {
        feedbackExportService: opts.feedbackExportService,
        pluginWorkerManager: workerManager,
    }));
    api.use(issueTreeControlRoutes(db));
    api.use(routineRoutes(db, { pluginWorkerManager: workerManager }));
    api.use(environmentRoutes(db, { pluginWorkerManager: workerManager }));
    api.use(executionWorkspaceRoutes(db));
    api.use(goalRoutes(db));
    api.use(approvalRoutes(db, { pluginWorkerManager: workerManager }));
    api.use(secretRoutes(db));
    api.use(costRoutes(db, { pluginWorkerManager: workerManager }));
    api.use(activityRoutes(db));
    api.use(dashboardRoutes(db));
    api.use(userProfileRoutes(db));
    api.use(sidebarBadgeRoutes(db));
    api.use(sidebarPreferenceRoutes(db));
    api.use(resourceMembershipRoutes(db));
    api.use(inboxDismissalRoutes(db));
    api.use(instanceSettingsRoutes(db));
    if (opts.databaseBackupService) {
        api.use(instanceDatabaseBackupRoutes(opts.databaseBackupService));
    }
    const pluginRegistry = pluginRegistryService(db);
    const eventBus = createPluginEventBus();
    setPluginEventBus(eventBus);
    const jobStore = pluginJobStore(db);
    const lifecycle = pluginLifecycleManager(db, { workerManager });
    const scheduler = createPluginJobScheduler({
        db,
        jobStore,
        workerManager,
    });
    const toolDispatcher = createPluginToolDispatcher({
        workerManager,
        lifecycleManager: lifecycle,
        db,
    });
    const jobCoordinator = createPluginJobCoordinator({
        db,
        lifecycle,
        scheduler,
        jobStore,
    });
    const hostServiceCleanup = createPluginHostServiceCleanup(lifecycle, hostServicesDisposers);
    let viteHtmlRenderer = null;
    const loader = pluginLoader(db, {
        localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
        migrationDb: opts.pluginMigrationDb,
    }, {
        workerManager,
        eventBus,
        jobScheduler: scheduler,
        jobStore,
        toolDispatcher,
        lifecycleManager: lifecycle,
        instanceInfo: {
            instanceId: opts.instanceId ?? "default",
            hostVersion: opts.hostVersion ?? "0.0.0",
            deploymentMode: opts.deploymentMode,
            deploymentExposure: opts.deploymentExposure,
        },
        buildHostHandlers: (pluginId, manifest) => {
            const notifyWorker = (method, params) => {
                const handle = workerManager.getWorker(pluginId);
                if (handle)
                    handle.notify(method, params);
            };
            const services = buildHostServices(db, pluginId, manifest.id, eventBus, notifyWorker, {
                pluginWorkerManager: workerManager,
                manifest,
            });
            hostServicesDisposers.set(pluginId, () => services.dispose());
            return createHostClientHandlers({
                pluginId,
                capabilities: manifest.capabilities,
                services,
            });
        },
    });
    api.use(pluginRoutes(db, loader, { scheduler, jobStore }, { workerManager }, { toolDispatcher }, { workerManager }));
    api.use(adapterRoutes());
    api.use(accessRoutes(db, {
        deploymentMode: opts.deploymentMode,
        deploymentExposure: opts.deploymentExposure,
        bindHost: opts.bindHost,
        allowedHostnames: opts.allowedHostnames,
    }));
    app.use("/api", api);
    app.use("/api", (_req, res) => {
        res.status(404).json({ error: "API route not found" });
    });
    app.use(pluginUiStaticRoutes(db, {
        localPluginDir: opts.localPluginDir ?? DEFAULT_LOCAL_PLUGIN_DIR,
    }));
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    if (opts.uiMode === "static") {
        // Try published location first (server/ui-dist/), then monorepo dev location (../../ui/dist)
        const candidates = [
            path.resolve(__dirname, "../ui-dist"),
            path.resolve(__dirname, "../../ui/dist"),
        ];
        const uiDist = candidates.find((p) => fs.existsSync(path.join(p, "index.html")));
        if (uiDist) {
            // Hashed asset files (Vite emits them under /assets/<name>.<hash>.<ext>)
            // never change once built, so they can be cached aggressively.
            app.use("/assets", express.static(path.join(uiDist, "assets"), {
                maxAge: "1y",
                immutable: true,
            }));
            // Non-hashed static files (favicon.ico, manifest, robots.txt, etc.):
            // short cache so operators who swap them out see the new version
            // reasonably fast. Override for `index.html` specifically — it is
            // served by this middleware for `/` and `/index.html`, and it must
            // never outlive the asset hashes it points at.
            app.use(express.static(uiDist, {
                maxAge: "1h",
                setHeaders(res, filePath) {
                    if (path.basename(filePath) === "index.html") {
                        res.set("Cache-Control", "no-cache");
                    }
                },
            }));
            // SPA fallback. Only for non-asset routes — if the browser asks for
            // /assets/something.js that doesn't exist, we must NOT serve the HTML
            // shell: the browser would try to load it as a JavaScript module, fail
            // with a MIME-type error, and cache that broken response. Return 404
            // instead. The index.html response itself is no-cache so a subsequent
            // deploy's updated asset hashes are picked up on next load.
            app.get(/.*/, (req, res) => {
                if (req.path.startsWith("/assets/")) {
                    res.status(404).end();
                    return;
                }
                res
                    .status(200)
                    .set("Content-Type", "text/html")
                    .set("Cache-Control", "no-cache")
                    .end(readBrandedStaticIndexHtml(uiDist));
            });
        }
        else {
            console.warn("[paperclip] UI dist not found; running in API-only mode");
        }
    }
    if (opts.uiMode === "vite-dev") {
        const uiRoot = path.resolve(__dirname, "../../ui");
        const publicUiRoot = path.resolve(uiRoot, "public");
        const hmrPort = resolveViteHmrPort(opts.serverPort);
        const hmrHost = resolveViteHmrHost(opts.bindHost);
        const { createServer: createViteServer } = await import("vite");
        const vite = await createViteServer({
            root: uiRoot,
            appType: "custom",
            server: {
                middlewareMode: true,
                hmr: {
                    ...(hmrHost ? { host: hmrHost } : {}),
                    port: hmrPort,
                    clientPort: hmrPort,
                },
                allowedHosts: privateHostnameGateEnabled ? Array.from(privateHostnameAllowSet) : undefined,
            },
        });
        viteHtmlRenderer = createCachedViteHtmlRenderer({
            vite,
            uiRoot,
            brandHtml: applyUiBranding,
        });
        const renderViteHtml = viteHtmlRenderer;
        if (fs.existsSync(publicUiRoot)) {
            app.use(express.static(publicUiRoot, { index: false }));
        }
        app.get(/.*/, async (req, res, next) => {
            if (!shouldServeViteDevHtml(req)) {
                next();
                return;
            }
            try {
                const html = await renderViteHtml.render(req.originalUrl);
                res.status(200).set({ "Content-Type": "text/html" }).end(html);
            }
            catch (err) {
                next(err);
            }
        });
        app.use(vite.middlewares);
    }
    app.use(errorHandler);
    jobCoordinator.start();
    scheduler.start();
    let feedbackExportShuttingDown = false;
    let feedbackExportTimer = null;
    const disableFeedbackExportFlushes = () => {
        feedbackExportShuttingDown = true;
        if (feedbackExportTimer) {
            clearInterval(feedbackExportTimer);
            feedbackExportTimer = null;
        }
    };
    const flushPendingFeedbackExports = async () => {
        if (feedbackExportShuttingDown)
            return;
        try {
            await opts.feedbackExportService?.flushPendingFeedbackTraces();
        }
        catch (err) {
            if (isDatabaseConnectionUnavailableError(err)) {
                disableFeedbackExportFlushes();
                logger.warn({ err }, "Disabling pending feedback export flushes because the database is unavailable");
                return;
            }
            logger.error({ err }, "Failed to flush pending feedback exports");
        }
    };
    feedbackExportTimer = opts.feedbackExportService
        ? setInterval(() => {
            void flushPendingFeedbackExports();
        }, FEEDBACK_EXPORT_FLUSH_INTERVAL_MS)
        : null;
    feedbackExportTimer?.unref?.();
    if (opts.feedbackExportService) {
        void flushPendingFeedbackExports();
    }
    void toolDispatcher.initialize().catch((err) => {
        logger.error({ err }, "Failed to initialize plugin tool dispatcher");
    });
    const devWatcher = createPluginDevWatcher(lifecycle, async (pluginId) => (await pluginRegistry.getById(pluginId))?.packagePath ?? null);
    void loader.loadAll().then((result) => {
        if (!result)
            return;
        for (const loaded of result.results) {
            if (devWatcher && loaded.success && loaded.plugin.packagePath) {
                devWatcher.watch(loaded.plugin.id, loaded.plugin.packagePath);
            }
        }
    }).catch((err) => {
        logger.error({ err }, "Failed to load ready plugins on startup");
    });
    let appServicesShutdown = false;
    const shutdownAppServices = () => {
        if (appServicesShutdown)
            return;
        appServicesShutdown = true;
        disableFeedbackExportFlushes();
        devWatcher?.close();
        viteHtmlRenderer?.dispose();
        hostServiceCleanup.disposeAll();
        hostServiceCleanup.teardown();
    };
    app.locals.paperclipShutdown = shutdownAppServices;
    process.once("exit", shutdownAppServices);
    process.once("beforeExit", () => {
        void flushPluginLogBuffer();
    });
    return app;
}
//# sourceMappingURL=app.js.map