import fs, { readdir } from "node:fs/promises";
import path from "node:path";
import { Type } from "@sinclair/typebox";
import { ApiError, Problems } from "@zuplo/errors";
import { normalizeDisplayPath, normalizeRelativePath } from "../utils/path.js";
async function fileExists(filepath) {
    try {
        await fs.access(filepath);
        return true;
    }
    catch {
        return false;
    }
}
async function dirExists(dirpath) {
    try {
        const stat = await fs.stat(dirpath);
        return stat.isDirectory();
    }
    catch {
        return false;
    }
}
const ReadDirQuerystring = Type.Object({
    path: Type.Optional(Type.String()),
    recursive: Type.Optional(Type.String()),
});
const ReadDirResponse = Type.Object({
    result: Type.Array(Type.Tuple([Type.String(), Type.Number()])),
    path: Type.String(),
});
const ReadFileQuerystring = Type.Object({
    path: Type.String(),
});
const WriteFileQuerystring = Type.Object({
    path: Type.String(),
});
const RenameFileQuerystring = Type.Object({
    path: Type.String(),
});
const RenameFileBody = Type.Object({
    newPath: Type.String(),
});
const DeleteFileQuerystring = Type.Object({
    path: Type.String(),
});
const CreateDirQuerystring = Type.Object({
    path: Type.String(),
});
export async function createFsRoutes(fastify, options) {
    const { sourceDirectory } = options;
    // Match portal's file API shape but simplified for local: /api/fs/dir
    fastify.get("/dir", {
        schema: {
            querystring: ReadDirQuerystring,
            response: {
                200: ReadDirResponse,
            },
        },
    }, async (request, reply) => {
        const requestPath = request.query.path ?? "/";
        const recursive = request.query.recursive === "true";
        const fileTree = [];
        // Helper function to recursively read directory
        const readDirRecursive = async (dirPath, basePath = "") => {
            try {
                const entries = await readdir(dirPath, { withFileTypes: true });
                for (const entry of entries) {
                    const relativePath = basePath
                        ? `${basePath}/${entry.name}`
                        : entry.name;
                    // Always prepend / to match portal's path format
                    const normalizedPath = normalizeDisplayPath(relativePath);
                    const fullPath = path.join(dirPath, entry.name);
                    if (entry.isDirectory()) {
                        // Type 2 = Directory
                        fileTree.push([normalizedPath, 2]);
                        if (recursive) {
                            await readDirRecursive(fullPath, relativePath);
                        }
                    }
                    else if (entry.isFile()) {
                        // Type 1 = File
                        fileTree.push([normalizedPath, 1]);
                    }
                }
            }
            catch (err) {
                // Skip directories that can't be read (permissions, etc)
                // but log the warning for debugging
                request.log.warn({ err, dirPath }, "Could not read directory, skipping");
            }
        };
        if (requestPath === "/") {
            // Read all directories - add parent dirs first
            const configPath = path.join(sourceDirectory, "config");
            if (await dirExists(configPath)) {
                fileTree.push(["/config", 2]);
                await readDirRecursive(configPath, "config");
            }
            const modulesPath = path.join(sourceDirectory, "modules");
            if (await dirExists(modulesPath)) {
                fileTree.push(["/modules", 2]);
                await readDirRecursive(modulesPath, "modules");
            }
            const schemasPath = path.join(sourceDirectory, "schemas");
            if (await dirExists(schemasPath)) {
                fileTree.push(["/schemas", 2]);
                await readDirRecursive(schemasPath, "schemas");
            }
            const testsPath = path.join(sourceDirectory, "tests");
            if (await dirExists(testsPath)) {
                fileTree.push(["/tests", 2]);
                await readDirRecursive(testsPath, "tests");
            }
            const docsPath = path.join(sourceDirectory, "docs");
            if (await dirExists(docsPath)) {
                fileTree.push(["/docs", 2]);
                await readDirRecursive(docsPath, "docs");
            }
        }
        else {
            // Read specific directory
            const fullPath = path.join(sourceDirectory, requestPath);
            if (await dirExists(fullPath)) {
                await readDirRecursive(fullPath, requestPath);
            }
        }
        // Return in backend's format: { path: string, result: FileList }
        reply.status(200).send({
            path: requestPath,
            result: fileTree,
        });
    });
    // Match portal's file API shape but simplified for local: /api/fs/file
    fastify.get("/file", {
        schema: {
            querystring: ReadFileQuerystring,
        },
    }, async (request, reply) => {
        const filePath = request.query.path;
        const relativePath = normalizeRelativePath(filePath);
        const fullPath = path.join(sourceDirectory, relativePath);
        if (!(await fileExists(fullPath))) {
            throw new ApiError({
                ...Problems.NOT_FOUND,
                detail: `Cannot find file: ${filePath}`,
            });
        }
        // Get file stats for Last-Modified header
        const stats = await fs.stat(fullPath);
        const content = await fs.readFile(fullPath, "utf-8");
        // Return raw content as text with Last-Modified header
        reply
            .status(200)
            .type("text/plain")
            .header("Last-Modified", stats.mtime.toUTCString())
            .send(content);
    });
    // Write/create a file: POST /file?path=...
    fastify.post("/file", {
        schema: {
            querystring: WriteFileQuerystring,
        },
    }, async (request, reply) => {
        const filePath = request.query.path;
        const relativePath = normalizeRelativePath(filePath);
        const fullPath = path.join(sourceDirectory, relativePath);
        // Check if file exists and has been modified since client loaded it
        // Skip conflict check if x-force-save header is present (user resolved conflict)
        const forceSave = request.headers["x-force-save"];
        const ifUnmodifiedSince = request.headers["if-unmodified-since"];
        if (!forceSave && ifUnmodifiedSince && (await fileExists(fullPath))) {
            const stats = await fs.stat(fullPath);
            // HTTP date headers only have second precision, so truncate to seconds for comparison
            const clientTimestamp = Math.floor(new Date(ifUnmodifiedSince).getTime() / 1000);
            const fileTimestamp = Math.floor(stats.mtime.getTime() / 1000);
            // If file timestamp doesn't match client's timestamp, check if content differs
            if (fileTimestamp !== clientTimestamp) {
                const newContent = typeof request.body === "string"
                    ? request.body
                    : JSON.stringify(request.body);
                const diskContent = await fs.readFile(fullPath, "utf-8");
                if (diskContent !== newContent) {
                    // Precondition failed - file was modified since client loaded it
                    throw new ApiError({
                        status: 412,
                        type: "https://zup.fail/http-status/412",
                        title: "Precondition Failed",
                        detail: "The file has been modified on disk since you loaded it.",
                        instance: filePath,
                    });
                }
            }
        }
        try {
            // Ensure parent directory exists
            const dir = path.dirname(fullPath);
            await fs.mkdir(dir, { recursive: true });
        }
        catch (err) {
            if (err.code === "ENOENT") {
                throw new ApiError({
                    ...Problems.NOT_FOUND,
                    detail: "Directory does not exist",
                });
            }
            throw err;
        }
        // Get the raw body content - handle text content type
        const content = typeof request.body === "string"
            ? request.body
            : JSON.stringify(request.body);
        await fs.writeFile(fullPath, content, "utf-8");
        // Get the new file timestamp after writing
        const newStats = await fs.stat(fullPath);
        // Return the new timestamp in Last-Modified header so client can update its value
        reply
            .status(200)
            .header("Last-Modified", newStats.mtime.toUTCString())
            .send({ buildId: `local-build-${Date.now()}` });
    });
    // Write/create multiple files: POST /files (multipart)
    fastify.post("/files", async (request, reply) => {
        if (!request.isMultipart()) {
            throw new ApiError({
                ...Problems.BAD_REQUEST,
                detail: "Request must be multipart/form-data",
            });
        }
        const parts = request.parts({ preservePath: true });
        const conflictedFiles = [];
        const savedFiles = {}; // Track saved files and their new timestamps
        for await (const part of parts) {
            if (part.type === "file" && part.filename) {
                const relativePath = normalizeRelativePath(part.filename);
                const fullPath = path.join(sourceDirectory, relativePath);
                const displayPath = normalizeDisplayPath(relativePath);
                // Get the content being saved
                const buffer = await part.toBuffer();
                const newContent = buffer.toString("utf-8");
                // Check for conflicts using timestamp and content comparison
                if (await fileExists(fullPath)) {
                    const headerKey = `x-if-unmodified-since-${encodeURIComponent(displayPath)}`.toLowerCase();
                    const ifUnmodifiedSince = request.headers[headerKey];
                    // If client provided a timestamp, check for conflicts
                    if (ifUnmodifiedSince) {
                        const stats = await fs.stat(fullPath);
                        // HTTP date headers only have second precision, so truncate to seconds for comparison
                        const clientTimestamp = Math.floor(new Date(ifUnmodifiedSince).getTime() / 1000);
                        const fileTimestamp = Math.floor(stats.mtime.getTime() / 1000);
                        // If file timestamp doesn't match client's timestamp, check if content differs
                        if (fileTimestamp !== clientTimestamp) {
                            const diskContent = await fs.readFile(fullPath, "utf-8");
                            if (diskContent !== newContent) {
                                // Precondition failed - file was modified since client loaded it
                                conflictedFiles.push(displayPath);
                                continue;
                            }
                        }
                    }
                }
                const dir = path.dirname(fullPath);
                await fs.mkdir(dir, { recursive: true });
                await fs.writeFile(fullPath, buffer);
                const newStats = await fs.stat(fullPath);
                savedFiles[displayPath] = newStats.mtime.getTime();
            }
        }
        if (conflictedFiles.length > 0) {
            throw new ApiError({
                status: 412,
                type: "https://zup.fail/http-status/412",
                title: "Precondition Failed",
                detail: `At least one file has been modified on disk since you loaded them.`,
                instance: JSON.stringify({ conflictedFiles }),
            });
        }
        reply.status(200).send({
            buildId: `local-build-${Date.now()}`,
            files: savedFiles,
        });
    });
    // Rename a file: PATCH /api/fs/file?path=...
    fastify.patch("/file", {
        schema: {
            querystring: RenameFileQuerystring,
            body: RenameFileBody,
        },
    }, async (request, reply) => {
        const oldPath = request.query.path;
        const { newPath } = request.body;
        const oldRelativePath = normalizeRelativePath(oldPath);
        const newRelativePath = normalizeRelativePath(newPath);
        const oldFullPath = path.join(sourceDirectory, oldRelativePath);
        const newFullPath = path.join(sourceDirectory, newRelativePath);
        if (!(await fileExists(oldFullPath))) {
            throw new ApiError({
                ...Problems.NOT_FOUND,
                detail: `Cannot find file: ${oldPath}`,
            });
        }
        await fs.rename(oldFullPath, newFullPath);
        reply.status(200).send({
            status: "redeploy successful",
            buildId: `local-build-${Date.now()}`,
        });
    });
    // Delete a file: DELETE /api/fs/delete?path=...
    fastify.delete("/delete", {
        schema: {
            querystring: DeleteFileQuerystring,
        },
    }, async (request, reply) => {
        const filePath = request.query.path;
        const relativePath = normalizeRelativePath(filePath);
        const fullPath = path.join(sourceDirectory, relativePath);
        if (!(await fileExists(fullPath))) {
            throw new ApiError({
                ...Problems.NOT_FOUND,
                detail: `Cannot find file: ${filePath}`,
            });
        }
        await fs.unlink(fullPath);
        reply.status(200).send({ buildId: `local-build-${Date.now()}` });
    });
    // Create a directory: POST /api/fs/dir?path=...
    fastify.post("/dir", {
        schema: {
            querystring: CreateDirQuerystring,
        },
    }, async (request, reply) => {
        const dirPath = request.query.path;
        const relativePath = normalizeRelativePath(dirPath);
        const fullPath = path.join(sourceDirectory, relativePath);
        await fs.mkdir(fullPath, { recursive: true });
        reply.status(201).send();
    });
}
//# sourceMappingURL=fs.js.map