#!/usr/bin/env node

import { spawn } from "node:child_process";
import { constants } from "node:fs";
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

interface CommandResult {
	code: number | null;
	signal: NodeJS.Signals | null;
	stdout: string;
	stderr: string;
	combined: string;
	durationMs: number;
}

interface StepDefinition {
	name: string;
	command: string;
	args: string[];
	cwd: string;
	expectedFiles?: string[];
}

interface RoutineOptions {
	projectName: string;
	themeRepo: string;
	themeDir: string;
	themeName: string;
	configFile: string;
	keepOnSuccess: boolean;
	keepOnFailure: boolean;
	verbose: boolean;
}

interface StepReport {
	step: string;
	commandLine: string;
	cwd: string;
	result: CommandResult;
}

interface HtmlAssertion {
	description: string;
	test: (html: string) => boolean;
}

const DEFAULT_OPTIONS: RoutineOptions = {
	projectName: "quickstart",
	themeRepo: "https://github.com/theNewDynamic/gohugo-theme-ananke.git",
	themeDir: "themes/ananke",
	themeName: "ananke",
	configFile: "hugo.toml",
	keepOnSuccess: false,
	keepOnFailure: true,
	verbose: true,
};

/**
 * Print CLI help.
 */
function printHelp(): void {
	console.log(
		`
Usage:
  node scripts/test-hugo-quickstart.ts [options]

Options:
  --project-name=<name>         Hugo project folder name inside the temp directory
  --theme-repo=<url>            Git URL for the theme submodule
  --theme-dir=<path>            Theme target directory inside the project
  --theme-name=<name>           Theme name written into hugo.toml
  --config-file=<file>          Hugo config file to update
  --keep-on-success             Do not delete the temp directory when the test passes
  --no-keep-on-failure          Delete the temp directory when the test fails
  --quiet                       Reduce step logging
  --help                        Show this help
`.trim(),
	);
}

/**
 * Parse CLI arguments into routine options.
 *
 * @param argv Raw CLI arguments after the executable and script path.
 * @returns Parsed routine options.
 * @throws Error when an unknown argument is passed.
 */
function parseArgs(argv: string[]): RoutineOptions {
	const options: RoutineOptions = { ...DEFAULT_OPTIONS };

	for (const arg of argv) {
		if (arg === "--help") {
			printHelp();
			process.exit(0);
		}

		if (arg === "--keep-on-success") {
			options.keepOnSuccess = true;
			continue;
		}

		if (arg === "--no-keep-on-failure") {
			options.keepOnFailure = false;
			continue;
		}

		if (arg === "--quiet") {
			options.verbose = false;
			continue;
		}

		if (arg.startsWith("--project-name=")) {
			options.projectName = arg.slice("--project-name=".length);
			continue;
		}

		if (arg.startsWith("--theme-repo=")) {
			options.themeRepo = arg.slice("--theme-repo=".length);
			continue;
		}

		if (arg.startsWith("--theme-dir=")) {
			options.themeDir = arg.slice("--theme-dir=".length);
			continue;
		}

		if (arg.startsWith("--theme-name=")) {
			options.themeName = arg.slice("--theme-name=".length);
			continue;
		}

		if (arg.startsWith("--config-file=")) {
			options.configFile = arg.slice("--config-file=".length);
			continue;
		}

		throw new Error(`Unknown argument: ${arg}`);
	}

	return options;
}

/**
 * Format a command for human-readable logging.
 *
 * @param command Executable name.
 * @param args Executable arguments.
 * @returns Full command line.
 */
function formatCommand(command: string, args: string[]): string {
	return [command, ...args]
		.map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
		.join(" ");
}

/**
 * Run a command and capture stdout/stderr.
 *
 * @param command Executable name.
 * @param args Executable arguments.
 * @param cwd Working directory.
 * @returns Command execution result.
 */
async function runCommand(
	command: string,
	args: string[],
	cwd: string,
): Promise<CommandResult> {
	const started = Date.now();

	return new Promise<CommandResult>((resolve, reject) => {
		const child = spawn(command, args, {
			cwd,
			env: process.env,
			stdio: ["ignore", "pipe", "pipe"],
		});

		let stdout = "";
		let stderr = "";

		child.stdout.on("data", (chunk: Buffer | string) => {
			stdout += chunk.toString();
		});

		child.stderr.on("data", (chunk: Buffer | string) => {
			stderr += chunk.toString();
		});

		child.on("error", (error: Error) => {
			reject(error);
		});

		child.on("close", (code, signal) => {
			const durationMs = Date.now() - started;
			const combined = [stdout, stderr]
				.filter(Boolean)
				.join(stdout && stderr ? "\n" : "");

			resolve({
				code,
				signal,
				stdout,
				stderr,
				combined,
				durationMs,
			});
		});
	});
}

/**
 * Ensure a file or directory exists.
 *
 * @param filePath Absolute path to check.
 */
async function assertFileExists(filePath: string): Promise<void> {
	await access(filePath, constants.F_OK);
}

/**
 * Ensure a file or directory does not exist.
 *
 * @param filePath Absolute path to check.
 */
async function assertFileDoesNotExist(filePath: string): Promise<void> {
	try {
		await access(filePath, constants.F_OK);
		throw new Error(`Unexpected path exists: ${filePath}`);
	} catch (error: unknown) {
		if (
			error instanceof Error &&
			error.message.startsWith("Unexpected path exists:")
		) {
			throw error;
		}
	}
}

/**
 * Read a UTF-8 text file.
 *
 * @param filePath Absolute file path.
 * @returns File contents.
 */
async function readTextFile(filePath: string): Promise<string> {
	return readFile(filePath, "utf8");
}

/**
 * Write a UTF-8 text file.
 *
 * @param filePath Absolute file path.
 * @param content File contents.
 */
async function writeTextFile(filePath: string, content: string): Promise<void> {
	await writeFile(filePath, content, "utf8");
}

/**
 * Remove the generated public directory inside the temporary project.
 *
 * @param projectRoot Absolute path to the temporary quickstart project.
 */
async function removePublicDir(projectRoot: string): Promise<void> {
	const publicPath = join(projectRoot, "public");
	await rm(publicPath, { recursive: true, force: true });
}

/**
 * Execute one step and validate success.
 *
 * @param step Step definition.
 * @returns Step report.
 * @throws Error when the command fails or an expected file is missing.
 */
async function executeStep(step: StepDefinition): Promise<StepReport> {
	const result = await runCommand(step.command, step.args, step.cwd);
	const commandLine = formatCommand(step.command, step.args);

	if (result.code !== 0) {
		const details = [
			`Step failed: ${step.name}`,
			`Command: ${commandLine}`,
			`Working directory: ${step.cwd}`,
			`Exit code: ${String(result.code)}`,
			result.signal ? `Signal: ${result.signal}` : "",
			result.stdout ? `STDOUT:\n${result.stdout}` : "",
			result.stderr ? `STDERR:\n${result.stderr}` : "",
		]
			.filter(Boolean)
			.join("\n\n");

		throw new Error(details);
	}

	if (step.expectedFiles) {
		for (const relativePath of step.expectedFiles) {
			const absolutePath = join(step.cwd, relativePath);

			try {
				await assertFileExists(absolutePath);
			} catch (error: unknown) {
				const message =
					error instanceof Error
						? error.message
						: "Unknown file assertion error";

				throw new Error(
					[
						`Step failed: ${step.name}`,
						`Command: ${commandLine}`,
						`Working directory: ${step.cwd}`,
						`Expected file missing: ${absolutePath}`,
						`Details: ${message}`,
						result.stdout ? `STDOUT:\n${result.stdout}` : "",
						result.stderr ? `STDERR:\n${result.stderr}` : "",
					]
						.filter(Boolean)
						.join("\n\n"),
				);
			}
		}
	}

	return {
		step: step.name,
		commandLine,
		cwd: step.cwd,
		result,
	};
}

/**
 * Determine whether a Hugo command generates output in `public/`.
 *
 * @param step Step definition.
 * @returns True when the command is a build command.
 */
function isHugoBuildCommand(step: StepDefinition): boolean {
	if (step.command !== "hugo") {
		return false;
	}

	if (step.args.length === 0) {
		return true;
	}

	if (step.args.includes("--buildDrafts")) {
		return true;
	}

	return false;
}

/**
 * Execute a Hugo build command after clearing the generated public directory.
 *
 * @param step Step definition.
 * @param projectRoot Absolute path to the temporary quickstart project.
 * @returns Step report.
 */
async function executeHugoBuildStep(
	step: StepDefinition,
	projectRoot: string,
): Promise<StepReport> {
	await removePublicDir(projectRoot);
	return executeStep(step);
}

/**
 * Escape a string for safe use in a regular expression.
 *
 * @param value Raw string.
 * @returns Escaped string.
 */
function escapeRegExp(value: string): string {
	return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
 * Assert that Hugo config contains the expected theme assignment somewhere in
 * the file, without requiring the whole config to match a fixed template.
 *
 * Accepts either single or double quotes, for example:
 * - theme = 'ananke'
 * - theme = "ananke"
 *
 * @param configPath Absolute config path.
 * @param themeName Expected theme name.
 * @throws Error when the config does not contain the expected theme line.
 */
async function assertThemeConfigured(
	configPath: string,
	themeName: string,
): Promise<void> {
	const config = await readTextFile(configPath);
	const themePattern = new RegExp(
		String.raw`^\s*theme\s*=\s*['"]${escapeRegExp(themeName)}['"]\s*$`,
		"m",
	);

	if (!themePattern.test(config)) {
		throw new Error(
			[
				"Strict assertion failed: theme configuration missing or incorrect.",
				`Config file: ${configPath}`,
				`Expected to find a line like: theme = '${themeName}'`,
				"Actual file contents:",
				config,
			].join("\n\n"),
		);
	}
}

/**
 * Return homepage assertions for the initial static build.
 *
 * @returns List of homepage assertions.
 */
function getHomepageAssertions(): HtmlAssertion[] {
	return [
		{
			description: "homepage contains an HTML document root",
			test: (html: string): boolean => /<html\b/i.test(html),
		},
		{
			description: "homepage contains a document title",
			test: (html: string): boolean => /<title>[\s\S]*?<\/title>/i.test(html),
		},
		{
			description: "homepage contains a body element",
			test: (html: string): boolean => /<body\b/i.test(html),
		},
		{
			description: "homepage contains at least one stylesheet reference",
			test: (html: string): boolean =>
				/<link\b[^>]*rel=["']stylesheet["'][^>]*>/i.test(html),
		},
		{
			description: "homepage contains at least one navigation-related landmark",
			test: (html: string): boolean => /<(nav|header)\b/i.test(html),
		},
		{
			description: "homepage contains theme-generated CSS class markers",
			test: (html: string): boolean =>
				/\b(ma[0-9]|pa[0-9]|bg-black|near-white|sans-serif)\b/i.test(html),
		},
		{
			description: "homepage contains a main content area",
			test: (html: string): boolean => /<(main|article|section)\b/i.test(html),
		},
	];
}

/**
 * Assert that the generated homepage looks like a real themed render.
 *
 * @param homepagePath Absolute path to `public/index.html`.
 * @throws Error when one or more assertions fail.
 */
async function assertHomepageLooksValid(homepagePath: string): Promise<void> {
	const html = await readTextFile(homepagePath);
	const failures: string[] = [];

	if (html.trim().length === 0) {
		throw new Error(
			[
				"Strict assertion failed: generated homepage is empty.",
				`Homepage file: ${homepagePath}`,
			].join("\n\n"),
		);
	}

	for (const assertion of getHomepageAssertions()) {
		if (!assertion.test(html)) {
			failures.push(`- ${assertion.description}`);
		}
	}

	if (failures.length > 0) {
		throw new Error(
			[
				"Strict assertion failed: generated homepage did not match expected render checks.",
				`Homepage file: ${homepagePath}`,
				"Failed assertions:",
				...failures,
			].join("\n"),
		);
	}
}

/**
 * Extract the auto-generated date line from a Hugo content file with TOML frontmatter.
 *
 * @param content Raw file contents.
 * @returns Original date line.
 * @throws Error when the date line is missing.
 */
function extractGeneratedDateLine(content: string): string {
	const match = content.match(/^\s*date\s*=\s*.+$/m);

	if (!match) {
		throw new Error(
			[
				"Strict assertion failed: could not find auto-generated date line in content file.",
				"Actual file contents:",
				content,
			].join("\n\n"),
		);
	}

	return match[0];
}

/**
 * Replace the generated content with the requested sample draft while preserving
 * the original date line created by `hugo new`.
 *
 * @param contentPath Absolute path to the content file.
 */
async function replaceGeneratedContent(contentPath: string): Promise<void> {
	const original = await readTextFile(contentPath);
	const dateLine = extractGeneratedDateLine(original);

	const updated = [
		"+++",
		"title = 'My First Post'",
		dateLine,
		"draft = true",
		"+++",
		"## Introduction",
		"",
		"This is **bold** text, and this is *emphasized* text.",
		"",
		"Visit the [Hugo](https://gohugo.io) website!",
		"",
	].join("\n");

	await writeTextFile(contentPath, updated);
}

/**
 * Replace the root Hugo config with the requested quickstart config.
 *
 * @param configPath Absolute path to `hugo.toml`.
 * @param themeName Theme name to set.
 */
async function replaceHugoConfig(
	configPath: string,
	themeName: string,
): Promise<void> {
	const content = [
		"baseURL = 'https://example.com/'",
		"locale = 'en-gb'",
		"title = 'Ananke Test Quickstart'",
		`theme = '${themeName}'`,
		"",
	].join("\n");

	await writeTextFile(configPath, content);
}

/**
 * Assert that the generated page contains the expected rendered draft content.
 *
 * @param pageHtml HTML from `public/foo/index.html`.
 */
function assertDraftPageRendered(pageHtml: string): void {
	const failures: string[] = [];

	if (!/<h2[^>]*>\s*Introduction\s*<\/h2>/i.test(pageHtml)) {
		failures.push("- heading 'Introduction' was not rendered as an h2 element");
	}

	if (!/<strong>\s*bold\s*<\/strong>/i.test(pageHtml)) {
		failures.push("- bold Markdown was not rendered as a <strong> element");
	}

	if (!/<em>\s*emphasized\s*<\/em>/i.test(pageHtml)) {
		failures.push("- emphasized Markdown was not rendered as an <em> element");
	}

	if (
		!/<a[^>]+href=["']https:\/\/gohugo\.io["'][^>]*>\s*Hugo\s*<\/a>/i.test(
			pageHtml,
		)
	) {
		failures.push("- Markdown link was not rendered as an anchor element");
	}

	if (!/My First Post/i.test(pageHtml)) {
		failures.push("- post title was not visible on the rendered page");
	}

	if (failures.length > 0) {
		throw new Error(
			[
				"Strict assertion failed: draft page content was not rendered as expected.",
				"Failed assertions:",
				...failures,
			].join("\n"),
		);
	}
}

/**
 * Assert that the generated homepage reflects updated title and locale configuration.
 *
 * Locale is checked strictly on the `<html>` tag.
 *
 * @param homepageHtml HTML from `public/index.html`.
 */
function assertUpdatedConfigInOutput(homepageHtml: string): void {
	const failures: string[] = [];

	if (!/Ananke Test Quickstart/i.test(homepageHtml)) {
		failures.push(
			"- updated site title was not visible in the generated output",
		);
	}

	if (!/<html[^>]+lang=["']en-gb["'][^>]*>/i.test(homepageHtml)) {
		failures.push(
			"- updated locale 'en-gb' was not present in the <html lang=\"en-gb\"> tag",
		);
	}

	if (failures.length > 0) {
		throw new Error(
			[
				"Strict assertion failed: updated config was not reflected in the generated output.",
				"Failed assertions:",
				...failures,
			].join("\n"),
		);
	}
}

/**
 * Assert that the draft page is not part of the production build.
 *
 * @param projectRoot Project root.
 * @param homepagePath Absolute path to `public/index.html`.
 */
async function assertDraftHiddenInProduction(
	projectRoot: string,
	homepagePath: string,
): Promise<void> {
	const draftOutputPath = join(projectRoot, "public", "foo", "index.html");
	await assertFileDoesNotExist(draftOutputPath);

	const homepageHtml = await readTextFile(homepagePath);

	if (/My First Post/i.test(homepageHtml)) {
		throw new Error(
			[
				"Strict assertion failed: draft post title was visible in the production homepage output.",
				`Homepage file: ${homepagePath}`,
			].join("\n\n"),
		);
	}
}

/**
 * Run the full Hugo quickstart verification routine.
 *
 * @param options Runtime options.
 * @returns Process exit code.
 */
async function runRoutine(options: RoutineOptions): Promise<number> {
	const sandboxRoot = await mkdtemp(join(tmpdir(), "hugo-quickstart-"));
	const projectRoot = join(sandboxRoot, options.projectName);

	const reports: StepReport[] = [];

	const steps: StepDefinition[] = [
		{
			name: "Create Hugo project",
			command: "hugo",
			args: ["new", "project", options.projectName],
			cwd: sandboxRoot,
			expectedFiles: [join(options.projectName, options.configFile)],
		},
		{
			name: "Initialise Git repository",
			command: "git",
			args: ["init"],
			cwd: projectRoot,
			expectedFiles: [".git"],
		},
		{
			name: "Add theme as Git submodule",
			command: "git",
			args: ["submodule", "add", options.themeRepo, options.themeDir],
			cwd: projectRoot,
			expectedFiles: [options.themeDir, ".gitmodules"],
		},
		{
			name: "Configure theme in Hugo config",
			command: "bash",
			args: [
				"-lc",
				`printf "\\ntheme = '${options.themeName}'\\n" >> ${JSON.stringify(options.configFile)}`,
			],
			cwd: projectRoot,
			expectedFiles: [options.configFile],
		},
		{
			name: "Build site",
			command: "hugo",
			args: [],
			cwd: projectRoot,
			expectedFiles: ["public/index.html"],
		},
	];

	try {
		console.log(`Test root: ${sandboxRoot}`);
		console.log(`Project root: ${projectRoot}`);

		for (const step of steps) {
			if (options.verbose) {
				console.log(`\n[RUN] ${step.name}`);
				console.log(`      ${formatCommand(step.command, step.args)}`);
			}

			const report = isHugoBuildCommand(step)
				? await executeHugoBuildStep(step, projectRoot)
				: await executeStep(step);

			reports.push(report);

			if (options.verbose) {
				console.log(
					`[OK ] ${step.name} (${report.result.durationMs} ms, exit ${String(report.result.code)})`,
				);

				const trimmedOutput = report.result.combined.trim();
				if (trimmedOutput) {
					console.log(trimmedOutput);
				}
			}
		}

		const configPath = join(projectRoot, options.configFile);
		const homepagePath = join(projectRoot, "public/index.html");
		const contentPath = join(projectRoot, "content/foo.md");
		const draftOutputPath = join(projectRoot, "public", "foo", "index.html");

		console.log("\n[RUN] Strict config assertion");
		await assertThemeConfigured(configPath, options.themeName);
		console.log("[OK ] Strict config assertion");

		console.log("\n[RUN] Strict homepage assertion");
		await assertHomepageLooksValid(homepagePath);
		console.log("[OK ] Strict homepage assertion");

		console.log("\n[RUN] Create sample content");
		const createContentStep: StepDefinition = {
			name: "Create sample content",
			command: "hugo",
			args: ["new", "foo.md"],
			cwd: projectRoot,
			expectedFiles: ["content/foo.md"],
		};
		const createContentReport = await executeStep(createContentStep);
		reports.push(createContentReport);

		if (options.verbose) {
			console.log(
				`      ${formatCommand(createContentStep.command, createContentStep.args)}`,
			);
			console.log(
				`[OK ] ${createContentStep.name} (${createContentReport.result.durationMs} ms, exit ${String(createContentReport.result.code)})`,
			);

			const trimmedOutput = createContentReport.result.combined.trim();
			if (trimmedOutput) {
				console.log(trimmedOutput);
			}
		}

		console.log("\n[RUN] Replace generated content with quickstart sample");
		await replaceGeneratedContent(contentPath);
		console.log("[OK ] Replace generated content with quickstart sample");

		console.log("\n[RUN] Build drafts and verify rendered draft content");
		const draftBuildStep: StepDefinition = {
			name: "Build site with drafts",
			command: "hugo",
			args: ["--buildDrafts"],
			cwd: projectRoot,
			expectedFiles: ["public/index.html", "public/foo/index.html"],
		};
		const draftBuildReport = await executeHugoBuildStep(
			draftBuildStep,
			projectRoot,
		);
		reports.push(draftBuildReport);

		if (options.verbose) {
			console.log(
				`      ${formatCommand(draftBuildStep.command, draftBuildStep.args)}`,
			);
			console.log(
				`[OK ] ${draftBuildStep.name} (${draftBuildReport.result.durationMs} ms, exit ${String(draftBuildReport.result.code)})`,
			);

			const trimmedOutput = draftBuildReport.result.combined.trim();
			if (trimmedOutput) {
				console.log(trimmedOutput);
			}
		}

		const draftPageHtml = await readTextFile(draftOutputPath);
		assertDraftPageRendered(draftPageHtml);
		console.log("[OK ] Build drafts and verify rendered draft content");

		console.log("\n[RUN] Replace root hugo.toml with quickstart config");
		await replaceHugoConfig(configPath, options.themeName);
		console.log("[OK ] Replace root hugo.toml with quickstart config");

		console.log("\n[RUN] Build drafts and verify updated title and locale");
		const configBuildStep: StepDefinition = {
			name: "Build site with updated config and drafts",
			command: "hugo",
			args: ["--buildDrafts"],
			cwd: projectRoot,
			expectedFiles: ["public/index.html", "public/foo/index.html"],
		};
		const configBuildReport = await executeHugoBuildStep(
			configBuildStep,
			projectRoot,
		);
		reports.push(configBuildReport);

		if (options.verbose) {
			console.log(
				`      ${formatCommand(configBuildStep.command, configBuildStep.args)}`,
			);
			console.log(
				`[OK ] ${configBuildStep.name} (${configBuildReport.result.durationMs} ms, exit ${String(configBuildReport.result.code)})`,
			);

			const trimmedOutput = configBuildReport.result.combined.trim();
			if (trimmedOutput) {
				console.log(trimmedOutput);
			}
		}

		const updatedHomepageHtml = await readTextFile(homepagePath);
		assertUpdatedConfigInOutput(updatedHomepageHtml);
		console.log("[OK ] Build drafts and verify updated title and locale");

		console.log("\n[RUN] Production build should exclude draft content");
		const productionBuildStep: StepDefinition = {
			name: "Build production site without drafts",
			command: "hugo",
			args: [],
			cwd: projectRoot,
			expectedFiles: ["public/index.html"],
		};
		const productionBuildReport = await executeHugoBuildStep(
			productionBuildStep,
			projectRoot,
		);
		reports.push(productionBuildReport);

		if (options.verbose) {
			console.log(
				`      ${formatCommand(productionBuildStep.command, productionBuildStep.args)}`,
			);
			console.log(
				`[OK ] ${productionBuildStep.name} (${productionBuildReport.result.durationMs} ms, exit ${String(productionBuildReport.result.code)})`,
			);

			const trimmedOutput = productionBuildReport.result.combined.trim();
			if (trimmedOutput) {
				console.log(trimmedOutput);
			}
		}

		await assertDraftHiddenInProduction(projectRoot, homepagePath);
		console.log("[OK ] Production build should exclude draft content");

		console.log("\nResult: PASS");

		if (options.keepOnSuccess) {
			console.log(`Keeping successful test directory: ${projectRoot}`);
		} else {
			await rm(sandboxRoot, { recursive: true, force: true });
			console.log(`Deleted successful test directory: ${sandboxRoot}`);
		}

		return 0;
	} catch (error: unknown) {
		const message = error instanceof Error ? error.message : "Unknown error";

		console.error("\nResult: FAIL");
		console.error(message);

		if (reports.length > 0) {
			console.error("\nCompleted command steps before failure:");
			for (const report of reports) {
				console.error(`- ${report.step}`);
			}
		}

		if (options.keepOnFailure) {
			console.error(
				`\nKept failing test directory for inspection: ${projectRoot}`,
			);
		} else {
			await rm(sandboxRoot, { recursive: true, force: true });
			console.error(`\nDeleted failing test directory: ${sandboxRoot}`);
		}

		return 1;
	}
}

/**
 * Main entry point.
 */
async function main(): Promise<void> {
	try {
		const options = parseArgs(process.argv.slice(2));
		const exitCode = await runRoutine(options);
		process.exit(exitCode);
	} catch (error: unknown) {
		const message =
			error instanceof Error ? error.message : "Unknown fatal error";
		console.error(`Fatal error: ${message}`);
		process.exit(1);
	}
}

await main();
