/* eslint-disable n/no-sync */
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import webpack from "webpack";

import { Extractor } from "./extractor.js";
import { Pot } from "./outputFormats/potEntry.js";

import type { I18nRow } from "./types/types.js";
import type { Asset, Compilation as CompilationType, Compiler } from "webpack";

const { Compilation, WebpackError, sources } = webpack;

class WebpackPluginRegexpTranslationExtractor {
    private logger: ReturnType<Compiler["getInfrastructureLogger"]>;

    private plugin = "WebpackPluginRegexpTranslationExtractor";

    private options: {
        outputFormat: Extractor["options"]["outputFormat"];
        functionNames: Extractor["options"]["functionNames"];
        /**
         * Creates a chunk map for all chunks for which the extractor has created a valid
         * output file. This can be useful e.g. if you want to split your translations for each
         * chunk into a single JSON file and lazy-load it.
         */
        createChunkMap?: {
            /**
             * The filename for the dependency map for each compilation. Use `%s` as
             * placeholder for the configuration name.
             */
            filename: string;
            filterChunk?: (chunkFilename: string) => boolean;
            filterDependency?: (dependency: string, asset: string) => boolean;
            mapDependency?: (dependency: string, asset: string) => string;
        };
    };

    public constructor(
        options: WebpackPluginRegexpTranslationExtractor["options"] = {
            outputFormat: Pot,
            functionNames: {
                single: "__",
                singleContext: "_x",
                plural: "_n",
                pluralContext: "_nx",
            },
        },
    ) {
        this.options = options;
    }

    public apply(compiler: Compiler) {
        const { plugin } = this;
        this.logger = compiler.getInfrastructureLogger(plugin);

        const createProcessAssetsTap = (
            compilation: CompilationType,
            stage: number,
            callback: (
                asset: Readonly<Asset>,
                nameWithoutQuery: string,
                assets: Parameters<Parameters<typeof compilation.hooks.processAssets.tap>[1]>[0],
            ) => void,
        ) => {
            // Collect strings
            compilation.hooks.processAssets.tap(
                {
                    name: plugin,
                    stage,
                    additionalAssets: true,
                },
                (assets) => {
                    Object.keys(assets).forEach((assetName) => {
                        const asset = compilation.getAsset(assetName);
                        const { name } = asset;
                        const [nameWithoutQuery] = name.split("?");
                        if (nameWithoutQuery.endsWith(".js")) {
                            try {
                                callback(asset, nameWithoutQuery, assets);
                            } catch (e) {
                                const { stack, message } = e as Error;
                                compilation.errors.push(
                                    new WebpackError(`${this.plugin}: ${name}: ${message}\n${stack}`),
                                );
                            }
                        }
                    });
                },
            );
        };

        compiler.hooks.thisCompilation.tap(plugin, (compilation) => {
            const { outputFormat, functionNames, createChunkMap } = this.options;
            const extractor = new Extractor({
                onValidateError: (msg) => compilation.errors.push(new WebpackError(msg)),
                outputFormat,
                beforeFunctionNameRegexp: `(?:\\(\\d+,[A-Za-z_-]+__WEBPACK_IMPORTED_MODULE_\\d+__\\.|[^A-Za-z0-9_])`,
                afterFunctionNameRegexp: `(?:\\)){0,1}`,
                functionNames,
            });
            const createdPotFilesForAsset: string[] = [];

            // stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
            // Collect strings for each asset
            const extractedCollection: Record<string, I18nRow[]> = {};
            createProcessAssetsTap(
                compilation,
                Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE,
                ({ source, name }, nameWithoutQuery) => {
                    const potFileName = `${nameWithoutQuery}.pot`;
                    if (compilation.getAsset(potFileName)) {
                        // Skip emitting the asset again because it's immutable
                        return;
                    }

                    const parsed = extractor.parse(nameWithoutQuery, source.source());

                    if (process.env.DEBUG_WRITE_EXTRACTION_FILE_TO_CWD) {
                        writeFileSync(
                            resolve(process.cwd(), `${name}.before-optimization.js`),
                            source.source().toString(),
                            {
                                encoding: "utf-8",
                            },
                        );
                    }

                    if (!parsed) {
                        // Validation failed, skip
                        return;
                    }

                    extractedCollection[nameWithoutQuery] = parsed;
                },
            );

            // Eliminate dead code translations
            createProcessAssetsTap(
                compilation,
                Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
                ({ source, name }, nameWithoutQuery) => {
                    const potFileName = `${nameWithoutQuery}.pot`;
                    if (compilation.getAsset(potFileName)) {
                        // Skip emitting the asset again because it's immutable
                        return;
                    }

                    const { [nameWithoutQuery]: parsed } = extractedCollection;
                    if (parsed) {
                        const sourceContent = source.source().toString("utf-8");

                        if (process.env.DEBUG_WRITE_EXTRACTION_FILE_TO_CWD) {
                            writeFileSync(
                                resolve(process.cwd(), `${name}.after-optimization.js`),
                                source.source().toString(),
                                {
                                    encoding: "utf-8",
                                },
                            );
                        }

                        const deadCodeEliminated = extractor.deadCodeElimination(parsed, sourceContent);
                        if (!deadCodeEliminated) {
                            return;
                        }

                        createdPotFilesForAsset.push(nameWithoutQuery);
                        compilation.emitAsset(
                            potFileName,
                            new sources.RawSource(extractor.createFileContent(deadCodeEliminated)),
                        );
                    }
                },
            );

            compilation.hooks.afterSeal.tap(plugin, () => {
                const { parsed, unique, deadCode, comments, files, timeMs } = extractor.getCounts();
                if (parsed > 0) {
                    this.logger.info(
                        `Extracted ${parsed} (unique: ${unique}, dead code eliminated: ${deadCode}) gettext function calls with ${comments} translator comments from ${files} assets in ${timeMs}ms!`,
                    );

                    if (createChunkMap) {
                        const { filename, filterChunk, filterDependency } = createChunkMap;
                        const chunkMapName = filename.replace(/%s/g, compilation.options.name);
                        const useFilename = resolve(compilation.outputOptions.path, chunkMapName);
                        const chunkMap = this.createChunkMap(
                            compilation,
                            createdPotFilesForAsset,
                            filterChunk,
                            filterDependency,
                        );

                        this.logger.info(`Additionally, created a translation chunk map ${chunkMapName}!`);

                        // `afterSeal` does not supporting emit new assets, so we need to manually write it
                        mkdirSync(dirname(useFilename), { recursive: true });
                        writeFileSync(useFilename, JSON.stringify(chunkMap, null, 4), { flag: "w" });
                    }
                }
            });
        });
    }

    private createChunkMap(
        compilation: CompilationType,
        createdPotFilesForAsset: string[],
        filterChunk?: WebpackPluginRegexpTranslationExtractor["options"]["createChunkMap"]["filterChunk"],
        filterDependency?: WebpackPluginRegexpTranslationExtractor["options"]["createChunkMap"]["filterDependency"],
        mapDependency?: WebpackPluginRegexpTranslationExtractor["options"]["createChunkMap"]["mapDependency"],
    ) {
        const result = {};

        compilation.chunks.forEach((chunk) => {
            for (const { files } of chunk.getAllReferencedChunks()) {
                for (const file of files) {
                    const [filenameWithoutQuery] = file.split("?");

                    if (!filterChunk?.(filenameWithoutQuery)) {
                        continue;
                    }

                    result[filenameWithoutQuery] = result[filenameWithoutQuery] || [];
                    result[filenameWithoutQuery].push(
                        ...[...chunk.files]
                            .map((f) => f.split("?")[0])
                            .filter((dependency) =>
                                filterDependency ? filterDependency(dependency, filenameWithoutQuery) : true,
                            )
                            .filter((dependency) => createdPotFilesForAsset.indexOf(dependency) > -1)
                            .filter(Boolean)
                            .map((dependency) =>
                                mapDependency ? mapDependency(dependency, filenameWithoutQuery) : dependency,
                            ),
                    );
                }
            }

            for (const key of Object.keys(result)) {
                result[key] = [...new Set(result[key])];
            }
        });

        return result;
    }
}

export { WebpackPluginRegexpTranslationExtractor };
