import fs from "node:fs/promises";
import path from "node:path";
import cors from "@fastify/cors";
import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import { watch } from "chokidar";
import Fastify from "fastify";
import { getLocalEditorConfig } from "./config.js";
import { editorConfig } from "./constants.js";
import { errorHandler } from "./error-handler.js";
import { createApiRoutes } from "./routes/api.js";
const staticFileExtensions = [".css", ".js", ".json", ".svg", ".png", ".ico"];
export class LocalEditorServer {
    app;
    listenerHost;
    watcher;
    sseClients = new Set();
    listenerPort;
    logger;
    deploymentUrl;
    sourceDirectory;
    constructor(options) {
        this.listenerPort = options.port;
        this.logger = options.logger;
        this.deploymentUrl = options.deploymentUrl;
        this.sourceDirectory = options.sourceDirectory;
        this.listenerHost = editorConfig.server.host;
        // Watch all project directories
        this.watcher = watch(editorConfig.watch.watchedDirs.map((dir) => path.join(this.sourceDirectory, dir)), { ignoreInitial: true });
        this.app = Fastify({
            // logger: this.logger,
            bodyLimit: editorConfig.server.bodyLimit,
            trustProxy: true,
        }).withTypeProvider();
    }
    init = async () => {
        this.logger.info({ sourceDirectory: this.sourceDirectory }, "Starting local editor server...");
        // Register multipart for file uploads
        await this.app.register(fastifyMultipart, {
            limits: {
                fileSize: editorConfig.file.maxFileSize,
            },
        });
        await this.app.register(cors, {
            origin: true,
            methods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS", "HEAD"],
            allowedHeaders: "*",
            exposedHeaders: "*",
        });
        // Set up custom error handler
        this.app.setErrorHandler(errorHandler);
        // Add support for text/plain and application/* content types
        this.app.addContentTypeParser("text/plain", { parseAs: "string" }, (_req, body, done) => {
            done(null, body);
        });
        this.app.addContentTypeParser("application/*", { parseAs: "buffer" }, (_req, body, done) => {
            done(null, body);
        });
        await this.app.register(createApiRoutes, {
            prefix: "/api",
            sourceDirectory: this.sourceDirectory,
            logger: this.logger,
            sseClients: this.sseClients,
            deploymentUrl: this.deploymentUrl,
        });
        // Serve static files from dist/client (for production/distributed mode)
        // In dev mode, Vite is in front and this won't be reached
        const clientDir = new URL("../dist/client", import.meta.url).pathname;
        await this.app.register(fastifyStatic, {
            root: clientDir,
            prefix: "/",
            // Disable automatic serving, we'll send this in the handler later
            serve: false,
            index: false,
        });
        // SPA fallback for production mode
        const htmlPath = new URL("../dist/client/index.html", import.meta.url)
            .pathname;
        let template;
        try {
            template = await fs.readFile(htmlPath, "utf-8");
        }
        catch {
            // If file doesn't exist yet (dev mode before build), just log and continue
            this.logger.info("Running in development mode.");
        }
        this.app.get("*", async (request, reply) => {
            // Only handle GET requests that accept HTML
            if (request.method !== "GET") {
                return reply.callNotFound();
            }
            if (!request.headers.accept?.includes("text/html") ||
                staticFileExtensions.some((ext) => request.url?.endsWith(ext))) {
                // biome-ignore lint/style/noNonNullAssertion: URL should be set
                return reply.sendFile(request.raw.url);
            }
            if (!template) {
                return reply.callNotFound();
            }
            const localEditorConfig = await getLocalEditorConfig({
                deploymentUrl: this.deploymentUrl,
                sourceDirectory: this.sourceDirectory,
                logger: this.logger,
            });
            // Inject localEditorConfig into the HTML
            const html = template.replace(`"{{localEditorConfig}}"`, // Note the quotes are correct
            JSON.stringify(localEditorConfig));
            reply.status(200).type("text/html").send(html);
        });
        // Setup file watcher
        this.watcher.on("change", async (filePath) => {
            const filePathRelative = path.relative(this.sourceDirectory, filePath);
            // Broadcast to all SSE clients
            const data = JSON.stringify({
                type: "change",
                filePath: filePathRelative,
            });
            for (const client of this.sseClients) {
                client.write(`event: change\ndata: ${data}\n\n`);
            }
            this.logger.info(`File changed: ${filePathRelative}`);
        });
    };
    start = async () => {
        await this.init();
        await this.app.listen({
            port: this.listenerPort,
            host: this.listenerHost,
        });
    };
    close = async () => {
        for (const client of this.sseClients) {
            try {
                if (client.writable) {
                    client.end();
                }
            }
            catch (err) {
                this.logger.warn({ err }, "Error closing SSE client");
            }
        }
        this.sseClients.clear();
        await this.watcher.close();
        await this.app.close();
    };
}
//# sourceMappingURL=server.js.map