/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

/**
 * Script to print the resolved ESLint configurations for various configurations and source file types.
 *
 * To add new configurations to print, add them to the `configsToPrint` array.
 *
 * For clarity, all the async file operations are done sequentially rather than collecting promises and using
 * `Promise.all`. This makes the code easier to read and is acceptable as this script is not performance critical.
 */

import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { ESLint } from "eslint";
import sortJson from "sort-json";

// Import flat configs directly from flat.mjs
import { recommended, strict } from "../flat.mjs";
import type { FlatConfigArray } from "../library/configs/base.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

interface ConfigToPrint {
	name: string;
	config: FlatConfigArray;
	sourceFilePath: string;
}

const configsToPrint = [
	{
		name: "default",
		config: recommended,
		sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
	},
	{
		name: "react",
		config: recommended,
		sourceFilePath: path.join(__dirname, "..", "src", "file.tsx"),
	},
	{
		name: "recommended",
		config: recommended,
		sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
	},
	{
		name: "strict",
		config: strict,
		sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
	},
	{
		name: "strict-biome",
		// strict-biome uses the same flat config as strict; biome integration is handled separately
		config: strict,
		sourceFilePath: path.join(__dirname, "..", "src", "file.ts"),
	},
	{
		name: "test",
		config: recommended,
		sourceFilePath: path.join(__dirname, "..", "src", "test", "file.ts"),
	},
] as const satisfies readonly ConfigToPrint[];

/**
 * Generates the applied ESLint config for a specific file and config.
 */
async function generateConfig(filePath: string, config: FlatConfigArray): Promise<string> {
	console.log(`Generating config for ${filePath}`);

	// ESLint 9's default ESLint class uses flat config format.
	// Use overrideConfigFile: true to prevent loading eslint.config.js,
	// and pass the config directly via overrideConfig.
	const eslint = new ESLint({
		overrideConfigFile: true,
		overrideConfig: [...config],
	});

	const resolvedConfig = (await eslint.calculateConfigForFile(filePath)) as unknown;
	if (!resolvedConfig) {
		console.warn("Warning: ESLint returned undefined config for " + filePath);
		return "{}\n";
	}

	// Serialize and parse to create a clean copy without any circular references or non-serializable values
	const cleanConfig = JSON.parse(JSON.stringify(resolvedConfig));

	// Remove globals from languageOptions (very large) but keep the rest (parserOptions, etc.)
	if (cleanConfig.languageOptions?.globals) {
		delete cleanConfig.languageOptions.globals;
	}

	// Remove tsconfigRootDir since it varies by environment (it's set to process.cwd())
	if (typeof cleanConfig.languageOptions?.parserOptions === "object") {
		delete cleanConfig.languageOptions.parserOptions.tsconfigRootDir;
	}

	// Convert numeric severities to string equivalents in rules
	if (cleanConfig.rules) {
		for (const [ruleName, ruleConfig] of Object.entries(cleanConfig.rules)) {
			if (Array.isArray(ruleConfig) && ruleConfig.length > 0) {
				const severity = ruleConfig[0];
				if (severity === 0) ruleConfig[0] = "off";
				else if (severity === 1) ruleConfig[0] = "warn";
				else if (severity === 2) ruleConfig[0] = "error";
			} else if (ruleConfig === 0 || ruleConfig === 1 || ruleConfig === 2) {
				// Handle standalone severity values
				const stringValue = ruleConfig === 0 ? "off" : ruleConfig === 1 ? "warn" : "error";
				cleanConfig.rules[ruleName] = stringValue;
			}
		}
	}

	// Generate the new content with sorting applied
	// Sorting at all is desirable as otherwise changes in the order of common config references may cause large diffs
	// with little semantic meaning.
	// On the other hand, fully sorting the json can be misleading:
	// some eslint settings depend on object key order ("import-x/resolver" being a known one, see
	// https://github.com/un-ts/eslint-plugin-import-x/blob/master/src/utils/resolve.ts).
	// Using depth 2 is a nice compromise.
	const sortedConfig = sortJson(cleanConfig, { depth: 2 });
	const finalConfig = JSON.stringify(sortedConfig, null, "\t");

	// Add a trailing newline to match preferred output formatting
	return finalConfig + "\n";
}

(async () => {
	const args = process.argv.slice(2);

	if (args.length !== 1) {
		console.error("Usage: jiti scripts/print-configs.ts <output-directory>");
		process.exit(1);
	}

	// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- validated by the args.length check above
	const outputPath = args[0]!;
	await fs.mkdir(outputPath, { recursive: true });
	const expectedFiles = new Set<string>();

	for (const { name, config, sourceFilePath } of configsToPrint) {
		const outputFilePath = path.join(outputPath, `${name}.json`);
		expectedFiles.add(`${name}.json`);

		let originalContent = "";
		try {
			originalContent = await fs.readFile(outputFilePath, "utf8");
		} catch (err) {
			// File doesn't exist yet, which is OK - we'll create it
		}

		const newContent = await generateConfig(sourceFilePath, config);

		// Only write the file if the content has changed
		if (newContent !== originalContent) {
			await fs.writeFile(outputFilePath, newContent);
		}
	}

	// Remove any files in the output directory that aren't in the expected list
	const existingFiles = await fs.readdir(outputPath);
	for (const file of existingFiles) {
		if (file.endsWith(".json") && !expectedFiles.has(file)) {
			console.log(`Removing unexpected file: ${file}`);
			await fs.unlink(path.join(outputPath, file));
		}
	}
})();
