/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as crypto from 'crypto';
import * as fs from 'fs';
import path from 'path';
import { dirs } from './dirs.ts';

export const root = fs.realpathSync.native(path.dirname(path.dirname(import.meta.dirname)));
export const stateFile = path.join(root, 'node_modules', '.postinstall-state');
export const stateContentsFile = path.join(root, 'node_modules', '.postinstall-state-contents');
export const forceInstallMessage = 'Run \x1b[36mnode build/npm/fast-install.ts --force\x1b[0m to force a full install.';

export function collectInputFiles(): string[] {
	const files: string[] = [];

	for (const dir of dirs) {
		const base = dir === '' ? root : path.join(root, dir);
		for (const file of ['package.json', 'package-lock.json', '.npmrc']) {
			const filePath = path.join(base, file);
			if (fs.existsSync(filePath)) {
				files.push(filePath);
			}
		}
	}

	files.push(path.join(root, '.nvmrc'));

	return files;
}

export interface PostinstallState {
	readonly nodeVersion: string;
	readonly fileHashes: Record<string, string>;
}

const packageJsonRelevantKeys = new Set([
	'name',
	'dependencies',
	'devDependencies',
	'optionalDependencies',
	'peerDependencies',
	'peerDependenciesMeta',
	'overrides',
	'engines',
	'workspaces',
	'bundledDependencies',
	'bundleDependencies',
]);

const packageLockJsonIgnoredKeys = new Set(['version']);

function normalizeFileContent(filePath: string): string {
	const raw = fs.readFileSync(filePath, 'utf8');
	const basename = path.basename(filePath);
	if (basename === 'package.json') {
		const json = JSON.parse(raw);
		const filtered: Record<string, unknown> = {};
		for (const key of packageJsonRelevantKeys) {
			// eslint-disable-next-line local/code-no-in-operator
			if (key in json) {
				filtered[key] = json[key];
			}
		}
		return JSON.stringify(filtered, null, '\t') + '\n';
	}
	if (basename === 'package-lock.json') {
		const json = JSON.parse(raw);
		for (const key of packageLockJsonIgnoredKeys) {
			delete json[key];
		}
		if (json.packages?.['']) {
			for (const key of packageLockJsonIgnoredKeys) {
				delete json.packages[''][key];
			}
		}
		return JSON.stringify(json, null, '\t') + '\n';
	}
	return raw;
}

function hashContent(content: string): string {
	const hash = crypto.createHash('sha256');
	hash.update(content);
	return hash.digest('hex');
}

export function computeState(): PostinstallState {
	const fileHashes: Record<string, string> = {};
	for (const filePath of collectInputFiles()) {
		const key = path.relative(root, filePath);
		try {
			fileHashes[key] = hashContent(normalizeFileContent(filePath));
		} catch {
			// file may not be readable
		}
	}
	return { nodeVersion: process.versions.node, fileHashes };
}

export function computeContents(): Record<string, string> {
	const fileContents: Record<string, string> = {};
	for (const filePath of collectInputFiles()) {
		try {
			fileContents[path.relative(root, filePath)] = normalizeFileContent(filePath);
		} catch {
			// file may not be readable
		}
	}
	return fileContents;
}

export function readSavedState(): PostinstallState | undefined {
	try {
		const { nodeVersion, fileHashes } = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
		return { nodeVersion, fileHashes };
	} catch {
		return undefined;
	}
}

export function isUpToDate(): boolean {
	const saved = readSavedState();
	if (!saved) {
		return false;
	}
	const current = computeState();
	return saved.nodeVersion === current.nodeVersion
		&& JSON.stringify(saved.fileHashes) === JSON.stringify(current.fileHashes);
}

export function readSavedContents(): Record<string, string> | undefined {
	try {
		return JSON.parse(fs.readFileSync(stateContentsFile, 'utf8'));
	} catch {
		return undefined;
	}
}

// When run directly, output state as JSON for tooling (e.g. the vscode-extras extension).
if (import.meta.filename === process.argv[1]) {
	if (process.argv[2] === '--normalize-file') {
		const filePath = process.argv[3];
		if (!filePath) {
			process.exit(1);
		}
		process.stdout.write(normalizeFileContent(filePath));
	} else {
		console.log(JSON.stringify({
			root,
			stateContentsFile,
			current: computeState(),
			saved: readSavedState(),
			files: [...collectInputFiles(), stateFile],
		}));
	}
}
