import { compile } from "../compiler/compile.js";
import { relative } from "node:path";
import { createHash } from "node:crypto";
import nodeFs from "node:fs";
import { Logger } from "../services/logger/index.js";
import { createTrackedFs, getWatchTargets, isPathWithinDirectories, } from "../services/file-watching/tracked-fs.js";
import { nodeNormalizePath } from "../utilities/node-normalize-path.js";
import { seedPreviousCompilationFromOutdir } from "../compiler/seed-previous-compilation.js";
const PLUGIN_NAME = "unplugin-paraglide-js";
const logger = new Logger();
/**
 * Default isServer which differs per bundler.
 */
let isServer;
// Module-scoped so the warm state survives plugin re-instantiation within
// one process (e.g. a vite config reload), but recreated when a different
// fs is passed — the tracked wrapper, read set, and cached compilation are
// only valid for the filesystem they were produced from.
let pluginState;
function getPluginState(args) {
    if (pluginState === undefined || pluginState.baseFs !== args.fs) {
        const tracked = createTrackedFs({ fs: args.fs });
        pluginState = {
            baseFs: args.fs,
            trackedFs: tracked.fs,
            readFiles: tracked.readFiles,
            clearReadFiles: tracked.clearReadFiles,
            previousCompilation: undefined,
            previousInputsDigest: undefined,
        };
    }
    return pluginState;
}
function withoutCleanOutdir(args) {
    const { cleanOutdir, ...compileArgs } = args;
    void cleanOutdir;
    return compileArgs;
}
/**
 * Hashes the files (and directory listings) the last compilation read,
 * together with the options that affect the output. Returns `undefined`
 * when the digest can't be computed (no tracked reads yet, an unexpected
 * read error, ...) — `undefined` never matches, so the caller compiles.
 *
 * Directory listings are included so that a message file *added* next to
 * the tracked ones invalidates the digest, not only edits to known files.
 * All components are length-prefixed so distinct input states can't
 * produce the same hash stream.
 *
 * The digest is taken after the compile, by re-reading the inputs. A file
 * edited *during* a compile can therefore be hashed at its new content
 * while the output reflects the old one — accepted: in dev, watchChange
 * recompiles on that edit, and a fresh build process always recompiles.
 */
async function computeInputsDigest(state, args, outputStructure) {
    if (state.readFiles.size === 0) {
        return undefined;
    }
    const targets = getWatchTargets(state.readFiles, { outdir: args.outdir });
    if (targets.files.size === 0) {
        return undefined;
    }
    const fsp = (args.fs ?? nodeFs).promises;
    const hash = createHash("sha256");
    try {
        const { fs: _fs, ...serializableArgs } = args;
        void _fs;
        hash.update(JSON.stringify({ ...serializableArgs, outputStructure, isServer }));
        for (const directoryPath of [...targets.directories].sort()) {
            const entries = await fsp
                .readdir(directoryPath)
                // tracked reads include probed-but-absent paths (the SDK reads
                // optional files and handles ENOENT itself) — a missing entry
                // is valid input state, hash it as such
                .catch(rethrowUnlessEnoent);
            hash.update(`\0dir:${directoryPath.length}:${directoryPath}:`);
            if (entries === undefined) {
                hash.update("missing");
            }
            else {
                for (const entry of [...entries].sort()) {
                    hash.update(`${entry.length}:${entry},`);
                }
            }
        }
        for (const filePath of [...targets.files].sort()) {
            const content = await fsp.readFile(filePath).catch(rethrowUnlessEnoent);
            hash.update(`\0file:${filePath.length}:${filePath}:`);
            if (content === undefined) {
                hash.update("missing");
            }
            else {
                hash.update(`${content.length}:`);
                hash.update(content);
            }
        }
    }
    catch {
        return undefined;
    }
    return hash.digest("hex");
}
function rethrowUnlessEnoent(error) {
    if (error?.code === "ENOENT") {
        return undefined;
    }
    throw error;
}
export const unpluginFactory = (args) => {
    const state = getPluginState(args);
    const { trackedFs, readFiles, clearReadFiles } = state;
    return {
        name: PLUGIN_NAME,
        enforce: "pre",
        async buildStart() {
            const isProduction = process.env.NODE_ENV === "production";
            // default to locale-modules for development to speed up the dev server
            // https://github.com/opral/inlang-paraglide-js/issues/486
            const outputStructure = args.outputStructure ??
                (isProduction ? "message-modules" : "locale-modules");
            try {
                // `vite build` calls buildStart once per environment (client, ssr).
                // Skip the expensive compile when the inputs haven't changed.
                if (state.previousCompilation && state.previousInputsDigest) {
                    const currentDigest = await computeInputsDigest(state, args, outputStructure);
                    if (currentDigest === state.previousInputsDigest) {
                        logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
                        return;
                    }
                }
                // On a fresh process, seed previousCompilation from on-disk hashes
                // so the first compile is a no-op when inputs are unchanged. Avoids
                // racing concurrent readers that wiping outdir would interrupt.
                const seededPrevious = state.previousCompilation ??
                    (await seedPreviousCompilationFromOutdir({
                        outdir: args.outdir,
                        fs: args.fs?.promises,
                    }));
                state.previousCompilation = await compile({
                    previousCompilation: seededPrevious,
                    outputStructure,
                    isServer,
                    ...withoutCleanOutdir(args),
                    cleanOutdir: false,
                    // after the args spread so a user-provided fs doesn't bypass
                    // the read tracking (trackedFs wraps args.fs when provided)
                    fs: trackedFs,
                });
                state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
                logger.success(`Compilation complete (${outputStructure})`);
            }
            catch (error) {
                state.previousInputsDigest = undefined;
                logger.error("Failed to compile project:", error.message);
                logger.info("Please check your translation files for syntax errors.");
                if (isProduction)
                    throw error;
            }
            finally {
                // in any case add the files to watch
                const targets = getWatchTargets(readFiles, { outdir: args.outdir });
                for (const filePath of targets.files) {
                    this.addWatchFile(filePath);
                }
                for (const directoryPath of targets.directories) {
                    this.addWatchFile(directoryPath);
                }
            }
        },
        async watchChange(path) {
            const normalizedPath = nodeNormalizePath(path);
            const targets = getWatchTargets(readFiles, { outdir: args.outdir });
            if (targets.isIgnoredPath(normalizedPath)) {
                return;
            }
            const shouldCompile = targets.files.has(normalizedPath) ||
                isPathWithinDirectories(normalizedPath, targets.directories);
            if (shouldCompile === false) {
                return;
            }
            const isProduction = process.env.NODE_ENV === "production";
            // default to locale-modules for development to speed up the dev server
            // https://github.com/opral/inlang-paraglide-js/issues/486
            const outputStructure = args.outputStructure ??
                (isProduction ? "message-modules" : "locale-modules");
            const previouslyReadFiles = new Set(readFiles);
            try {
                logger.info(`Re-compiling inlang project... File "${relative(process.cwd(), path)}" has changed.`);
                // Clear readFiles to track fresh file reads
                clearReadFiles();
                state.previousCompilation = await compile({
                    previousCompilation: state.previousCompilation,
                    outputStructure,
                    isServer,
                    ...withoutCleanOutdir(args),
                    cleanOutdir: false,
                    fs: trackedFs,
                });
                state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
                logger.success(`Re-compilation complete (${outputStructure})`);
                // Add any new files to watch
                const nextTargets = getWatchTargets(readFiles, { outdir: args.outdir });
                for (const filePath of nextTargets.files) {
                    this.addWatchFile(filePath);
                }
                for (const directoryPath of nextTargets.directories) {
                    this.addWatchFile(directoryPath);
                }
            }
            catch (e) {
                clearReadFiles();
                for (const filePath of previouslyReadFiles) {
                    readFiles.add(filePath);
                }
                // Reset compilation result on error
                state.previousCompilation = undefined;
                state.previousInputsDigest = undefined;
                logger.warn("Failed to re-compile project:", e.message);
            }
        },
        vite: {
            config: {
                handler: () => {
                    isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
                },
            },
            configEnvironment: {
                handler: () => {
                    isServer = "import.meta.env?.SSR ?? typeof window === 'undefined'";
                },
            },
        },
        webpack(compiler) {
            compiler.options.resolve = {
                ...compiler.options.resolve,
                fallback: {
                    ...compiler.options.resolve?.fallback,
                    // https://stackoverflow.com/a/72989932
                    async_hooks: false,
                },
            };
            compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
                const isProduction = process.env.NODE_ENV === "production";
                // default to locale-modules for development to speed up the dev server
                // https://github.com/opral/inlang-paraglide-js/issues/486
                const outputStructure = args.outputStructure ??
                    (isProduction ? "message-modules" : "locale-modules");
                try {
                    // Multi-compiler webpack setups (client + server) trigger
                    // beforeRun once per compiler — skip when inputs are unchanged.
                    if (state.previousCompilation && state.previousInputsDigest) {
                        const currentDigest = await computeInputsDigest(state, args, outputStructure);
                        if (currentDigest === state.previousInputsDigest) {
                            logger.info(`Compilation skipped — inputs unchanged (${outputStructure})`);
                            return;
                        }
                    }
                    const seededPrevious = state.previousCompilation ??
                        (await seedPreviousCompilationFromOutdir({
                            outdir: args.outdir,
                            fs: args.fs?.promises,
                        }));
                    state.previousCompilation = await compile({
                        previousCompilation: seededPrevious,
                        outputStructure,
                        ...withoutCleanOutdir(args),
                        cleanOutdir: false,
                        fs: trackedFs,
                    });
                    state.previousInputsDigest = await computeInputsDigest(state, args, outputStructure);
                    logger.success(`Compilation complete (${outputStructure})`);
                }
                catch (error) {
                    state.previousInputsDigest = undefined;
                    logger.warn("Failed to compile project:", error.message);
                    logger.warn("Please check your translation files for syntax errors.");
                    if (isProduction)
                        throw error;
                }
            });
        },
    };
};
//# sourceMappingURL=unplugin.js.map