/// <reference types="node" />

/**
 * @file Lit Localize build script.
 *
 * @remarks
 * Determines if all the Xliff translation source files are present and
 * if the Typescript source files generated from those sources are up-to-date.
 *
 * If they are not, it runs the locale building script, intercepting the
 * long spew of "this string is not translated" and replacing it with a
 * summary of how many strings are missing with respect to the source locale.
 *
 * @import { Stats } from "node:fs";
 */

import * as fs from "node:fs/promises";
import path, { resolve } from "node:path";

import { generatePseudoLocaleModule } from "./pseudolocalize.mjs";

import { ConsoleLogger } from "#logger/node";
import { PackageRoot } from "#paths/node";

import { readConfigFileAndWriteSchema } from "@lit/localize-tools/lib/config.js";
import { RuntimeLitLocalizer } from "@lit/localize-tools/lib/modes/runtime.js";

//#region Setup

const missingMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sis\smissing/;
const outdatedMessagePattern = /([\w_-]+)\smessage\s(?:[\w_.-]+)\sdoes\snot\sexist/;
const logger = ConsoleLogger.child({ name: "Locales" });

const localizeRules = readConfigFileAndWriteSchema(path.join(PackageRoot, "lit-localize.json"));

if (localizeRules.interchange.format !== "xliff") {
    logger.error("Unsupported interchange type, expected 'xliff'");
    process.exit(1);
}

const { sourceLocale } = localizeRules;

localizeRules.targetLocales = localizeRules.targetLocales.filter((locale) => {
    return locale !== sourceLocale;
});

const XLIFFPath = resolve(PackageRoot, localizeRules.interchange.xliffDir);

const EmittedLocalesDirectory = resolve(
    PackageRoot,
    /** @type {string} */ (localizeRules.output.outputDir),
);

const targetLocales = localizeRules.targetLocales.filter((localeCode) => {
    return localeCode !== "en-XA";
});

//#endregion

//#region Utilities

/**
 * Cleans the emitted locales directory.
 */
async function cleanEmittedLocales() {
    logger.info("♻️ Cleaning previously emitted locales...");
    logger.info(`♻️ ${EmittedLocalesDirectory}`);

    await fs.rm(EmittedLocalesDirectory, {
        recursive: true,
        force: true,
    });

    await fs.mkdir(EmittedLocalesDirectory, {
        recursive: true,
    });

    logger.info(`♻️ Done!`);
}

/**
 * Returns false if: the expected XLF file doesn't exist, The expected
 * generated file doesn't exist, or the XLF file is newer (has a higher date)
 * than the generated file.  The missing XLF file is important enough it
 * generates a unique error message and halts the build.
 *
 * @param {string} localeCode
 * @returns {Promise<boolean>}
 */
async function checkIfEmittedFileCurrent(localeCode) {
    const xliffPath = path.join(XLIFFPath, `${localeCode}.xlf`);
    const emittedPath = path.join(EmittedLocalesDirectory, `${localeCode}.ts`);

    /**
     * @type {Stats}
     */
    let xliffStat;

    try {
        xliffStat = await fs.stat(xliffPath);
    } catch (_error) {
        logger.error(`XLIFF source file missing for locale '${localeCode}': ${xliffPath}`);
        process.exit(1);
    }

    /**
     * @type {Stats}
     */
    let emittedStat;

    // If the generated file doesn't exist, of course it's not up to date.
    try {
        emittedStat = await fs.stat(emittedPath);
    } catch (_error) {
        return false;
    }

    // Possible if the script was interrupted between clearing and generating.
    if (emittedStat.size === 0) {
        return false;
    }

    // If the emitted file is the same age or newer (date is greater) than the xliff file, it's
    // presumed to have been generated by that file and is up-to-date.
    return emittedStat.mtimeMs >= xliffStat.mtimeMs;
}

/**
 * Checks if all the locale source files are up-to-date with their XLIFF sources.
 * @returns {Promise<boolean>}
 */
async function checkIfLocalesAreCurrent() {
    logger.info("Reading locale configuration...");

    logger.info(`Checking ${targetLocales.length} source files...`);

    let outOfDateCount = 0;

    await Promise.all(
        targetLocales.map(async (localeCode) => {
            const current = await checkIfEmittedFileCurrent(localeCode);

            if (!current) {
                logger.info(`Locale '${localeCode}' is out-of-date.`);
                outOfDateCount++;
            }
        }),
    );

    return outOfDateCount === 0;
}

export async function generateLocaleModules() {
    logger.info("Updating pseudo-locale...");
    await generatePseudoLocaleModule();

    logger.info("Generating locale modules...");

    /**
     * @type {Map<string, number>}
     */
    const missingTranslationWarnings = new Map();

    /**
     * @type {Map<string, number>}
     */
    const outdatedTranslationWarnings = new Map();

    const initialConsoleWarn = console.warn;

    console.warn = (arg0, ...args) => {
        if (typeof arg0 !== "string") {
            initialConsoleWarn(arg0, ...args);
            return;
        }

        const [, matchedMissingTranslation] = arg0.match(missingMessagePattern) || [];

        if (matchedMissingTranslation) {
            const count = missingTranslationWarnings.get(matchedMissingTranslation) || 0;

            missingTranslationWarnings.set(matchedMissingTranslation, count + 1);

            logger.debug(arg0);

            return;
        }

        const [, matchedOutdatedTranslation] = arg0.match(outdatedMessagePattern) || [];

        if (matchedOutdatedTranslation) {
            const count = outdatedTranslationWarnings.get(matchedOutdatedTranslation) || 0;

            outdatedTranslationWarnings.set(matchedOutdatedTranslation, count + 1);

            logger.debug(arg0);

            return;
        }

        initialConsoleWarn(arg0, ...args);
    };

    // @ts-expect-error: Type is too broad.
    const localizer = new RuntimeLitLocalizer(localizeRules);

    await localizer.build();

    const missingTranslationsReport = Array.from(missingTranslationWarnings)
        .filter(([, count]) => count)
        .sort(([, totalsA], [, totalsB]) => {
            return totalsB - totalsA;
        })
        .map(([locale, count]) => `${locale}: ${count.toLocaleString()}`)
        .join("\n");

    logger.info(`Missing translations:\n${missingTranslationsReport || "None"}`);

    const outdatedTranslationsReport = Array.from(outdatedTranslationWarnings)
        .filter(([, count]) => count)
        .sort(([, totalsA], [, totalsB]) => {
            return totalsB - totalsA;
        })
        .map(([locale, count]) => `${locale}: ${count.toLocaleString()}`)
        .join("\n");

    logger.info(`Outdated translations:\n${outdatedTranslationsReport || "None"}`);

    localizer.assertTranslationsAreValid();

    logger.info("Complete.");
}

//#endregion

//#region Commands

async function delegateCommand() {
    const command = process.argv[2];

    switch (command) {
        case "--clean":
            return cleanEmittedLocales();
        case "--check":
            return checkIfLocalesAreCurrent();
        case "--force":
            return cleanEmittedLocales().then(generateLocaleModules);
    }

    const upToDate = await checkIfLocalesAreCurrent();

    if (upToDate) {
        logger.info("Locale is up-to-date!");

        return;
    }

    logger.info("Locale ./src is out-of-date, rebuilding...");

    return generateLocaleModules();
}

await delegateCommand()
    .then(() => {
        process.exit(0);
    })
    .catch((error) => {
        logger.error(`Error during locale build: ${error}`);
        process.exit(1);
    });

//#endregion
