/**
 * Virtual file handler for API files.
 *
 * APIs appear as single TypeScript files (apis/MyAPI.ts) but are stored
 * as directories containing YAML definitions and step files.
 */
import { readFile, mkdir, writeFile, access, rm, readdir, stat, } from "node:fs/promises";
import * as path from "node:path";
import { getLogger, getErrorMeta } from "../../../../util/logger.js";
import { Paths } from "../../../const.js";
import { YamlToApiBuilderTransformer } from "../../../transform/api-builder/to-sdk-transformer.js";
import { matchPath, matchMultiplePaths, renderPath, applyFileTransformations, } from "../../../transform/shared.js";
// Path matchers
const generatedApisMatch = matchPath(Paths.GeneratedApis);
const apiArtifactsMatchRaw = matchMultiplePaths("apis/:apiName", "apis/:apiName/", "apis/:apiName/**");
/**
 * Matches physical API artifact paths (directories and files inside API dirs).
 * Excludes virtual paths like apis/MyAPI.ts.
 */
function matchPhysicalApiPath(physicalPath) {
    const match = apiArtifactsMatchRaw(physicalPath);
    if (match.matched && !match.params.apiName.endsWith(".ts")) {
        return match;
    }
    return { matched: false };
}
/**
 * Handler for virtual API files.
 *
 * Virtual path: apis/MyAPI.ts
 * Physical storage: apis/MyAPI/api.yaml + step files
 * Scratch location: .superblocks/scratch/apis%2FMyAPI.ts
 */
export class ApiFileHandler {
    type = "api";
    logger = getLogger();
    matches(virtualPath) {
        const match = generatedApisMatch(virtualPath);
        return match.matched && match.params.apiName.endsWith(".ts");
    }
    matchesPhysical(physicalPath) {
        return matchPhysicalApiPath(physicalPath).matched;
    }
    parse(virtualPath) {
        const match = generatedApisMatch(virtualPath);
        if (match.matched && match.params.apiName.endsWith(".ts")) {
            return {
                type: this.type,
                params: {
                    apiName: path.basename(match.params.apiName, ".ts"),
                },
            };
        }
        return undefined;
    }
    toVirtualPath(physicalPath) {
        const match = matchPhysicalApiPath(physicalPath);
        if (match.matched) {
            return renderPath(Paths.GeneratedApis, {
                apiName: `${match.params.apiName}.ts`,
            });
        }
        return undefined;
    }
    async read(virtualPath, options) {
        const info = this.parse(virtualPath);
        if (!info)
            return undefined;
        const { appRootDirPath, templateRenderer, entityNames = [] } = options;
        const { apiName } = info.params;
        if (!apiName)
            return undefined;
        // Build paths
        const scratchPath = renderPath(Paths.Scratch, { path: virtualPath });
        const fullScratchPath = path.join(appRootDirPath, scratchPath);
        const yamlPath = renderPath(Paths.ApiYamls, { apiName });
        const fullYamlPath = path.join(appRootDirPath, yamlPath);
        // Check if both scratch and YAML exist, and compare timestamps
        // If YAML is newer than scratch, the scratch is stale (user made manual edits)
        let scratchExists = false;
        let scratchMtime;
        let yamlExists = false;
        let yamlMtime;
        try {
            const scratchStat = await stat(fullScratchPath);
            scratchExists = true;
            scratchMtime = scratchStat.mtime;
        }
        catch {
            this.logger.debug(`Scratch file does not exist: ${fullScratchPath}`);
        }
        try {
            const yamlStat = await stat(fullYamlPath);
            yamlExists = true;
            yamlMtime = yamlStat.mtime;
        }
        catch {
            this.logger.debug(`YAML file does not exist: ${fullYamlPath}`);
        }
        // If scratch exists and is newer than YAML (or YAML doesn't exist), use scratch
        const scratchIsFresh = scratchExists &&
            scratchMtime &&
            (!yamlExists || !yamlMtime || scratchMtime >= yamlMtime);
        if (scratchIsFresh) {
            return await readFile(fullScratchPath, "utf-8");
        }
        // YAML doesn't exist and scratch is stale or missing
        if (!yamlExists) {
            this.logger.debug(`YAML file does not exist: ${fullYamlPath}`);
            return undefined;
        }
        if (!templateRenderer) {
            throw new Error("TemplateRenderer is required to generate API content from YAML");
        }
        // Generate from YAML (scratch is stale or doesn't exist)
        const yamlArtifact = {
            type: "file",
            filePath: yamlPath,
            content: "",
        };
        const [sdkArtifact] = await applyFileTransformations([yamlArtifact], [
            new YamlToApiBuilderTransformer({
                appRootDirPath,
                entityNames,
                skipCleanup: true,
                templateRenderer,
            }),
        ]);
        // Cache the transformed result to scratch for subsequent reads
        if (sdkArtifact?.content) {
            try {
                await mkdir(path.dirname(fullScratchPath), { recursive: true });
                await writeFile(fullScratchPath, sdkArtifact.content, "utf-8");
            }
            catch (err) {
                // Cache write failure should not break the read operation
                getLogger().warn(`Failed to cache transformed API to scratch: ${fullScratchPath}`, getErrorMeta(err));
            }
        }
        return sdkArtifact?.content;
    }
    async write(virtualPath, content, options) {
        const info = this.parse(virtualPath);
        if (!info) {
            throw new Error(`Invalid API path: ${virtualPath}`);
        }
        const { appRootDirPath } = options;
        const scratchPath = renderPath(Paths.Scratch, { path: virtualPath });
        const fullScratchPath = path.join(appRootDirPath, scratchPath);
        await mkdir(path.dirname(fullScratchPath), { recursive: true });
        await writeFile(fullScratchPath, content, "utf-8");
    }
    async delete(virtualPath, options) {
        const info = this.parse(virtualPath);
        if (!info) {
            throw new Error(`Invalid API path: ${virtualPath}`);
        }
        const { appRootDirPath, createDraftDelete } = options;
        const { apiName } = info.params;
        if (!apiName) {
            throw new Error(`Missing apiName in parsed path: ${virtualPath}`);
        }
        const deletedPaths = [];
        // Delete API directory contents
        const apiDirPath = renderPath(Paths.GeneratedApis, { apiName });
        const absoluteApiDirPath = path.join(appRootDirPath, apiDirPath);
        try {
            await access(absoluteApiDirPath);
            await this.deleteDirectoryContents(absoluteApiDirPath, appRootDirPath, createDraftDelete, deletedPaths);
            await rm(absoluteApiDirPath, { recursive: true, force: true });
        }
        catch {
            // API directory doesn't exist
        }
        // Delete scratch file if it exists
        const scratchPath = renderPath(Paths.Scratch, { path: virtualPath });
        try {
            await createDraftDelete(scratchPath);
            deletedPaths.push(scratchPath);
        }
        catch {
            // Scratch file doesn't exist
        }
        return deletedPaths;
    }
    /**
     * Gets metadata for a virtual API file.
     * Checks scratch file first (for pending changes), then falls back to YAML.
     *
     * @param virtualPath - The virtual API path (e.g., "apis/MyAPI.ts")
     * @param options - Read options including app root
     * @returns FileMetadata or undefined if not found
     */
    async getMetadata(virtualPath, options) {
        const info = this.parse(virtualPath);
        if (!info)
            return undefined;
        const { appRootDirPath } = options;
        const { apiName } = info.params;
        if (!apiName)
            return undefined;
        // Check scratch file first (for pending changes)
        const scratchPath = renderPath(Paths.Scratch, { path: virtualPath });
        const fullScratchPath = path.join(appRootDirPath, scratchPath);
        try {
            const stats = await stat(fullScratchPath);
            return {
                path: virtualPath,
                size: stats.size,
                mtime: stats.mtime,
                isDirectory: false,
                isFile: true,
            };
        }
        catch {
            // Fall back to YAML file
        }
        // Check YAML file
        const yamlPath = renderPath(Paths.ApiYamls, { apiName });
        const fullYamlPath = path.join(appRootDirPath, yamlPath);
        try {
            const stats = await stat(fullYamlPath);
            return {
                path: virtualPath,
                size: stats.size,
                mtime: stats.mtime,
                isDirectory: false,
                isFile: true,
            };
        }
        catch {
            return undefined;
        }
    }
    async deleteDirectoryContents(dirPath, appRootDirPath, createDraftDelete, deletedPaths) {
        const entries = await readdir(dirPath, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = path.join(dirPath, entry.name);
            const relativePath = path.relative(appRootDirPath, fullPath);
            if (entry.isDirectory()) {
                await this.deleteDirectoryContents(fullPath, appRootDirPath, createDraftDelete, deletedPaths);
            }
            else {
                await createDraftDelete(relativePath);
                deletedPaths.push(relativePath);
            }
        }
    }
}
//# sourceMappingURL=api-handler.js.map