import type {ArtifactGenerationConfig} from './types.js';
import fs from 'node:fs';
import path from 'node:path';
import {FileTraversed, slash, traverse} from './utils/files.js';

// TODO option
const GENERATE_EMPTY_ARTIFACTS: boolean = false;
const REF_ARTIFACT_ABI = GENERATE_EMPTY_ARTIFACTS ? true : false;

function writeIfDifferent(filePath: string, newTextContent: string) {
	// Ensure we're working with a string
	const contentToWrite = String(newTextContent);

	try {
		let existingContent;

		try {
			existingContent = fs.readFileSync(filePath, 'utf8');
		} catch (error) {
			// console.log(`do not exist? => writing ${filePath}`);
			// File doesn't exist, write and return
			fs.writeFileSync(filePath, contentToWrite);
			return {written: true, reason: 'File did not exist'};
		}

		// Simple string comparison
		if (contentToWrite !== existingContent) {
			// console.log(`content different => writing ${filePath}`);
			fs.writeFileSync(filePath, contentToWrite);
			return {written: true, reason: 'Content was different'};
		}

		return {written: false, reason: 'Content was identical'};
	} catch (error) {
		console.error('Error in writeIfDifferent:', error);
		throw error;
	}
}

function ensureDirExistsSync(folderPath: string) {
	// Check if directory already exists
	if (fs.existsSync(folderPath)) {
		return {created: false, reason: 'Directory already exists'};
	}

	// console.log(`do not exist? => mkdir ${folderPath}`);
	// Directory doesn't exist, create it
	fs.mkdirSync(folderPath, {recursive: true});
	return {created: true, reason: 'Directory was created'};
}

type Artifact = {
	contractName: string;
	abi: any[];
	// ...
};

type Artifacts = {[key: string]: Artifact};

function writeArtifactToFile(folder: string, canonicalName: string, data: Artifact, mode: 'typescript' | 'javascript') {
	const name = canonicalName.split('/').pop();
	const artifactName = `Artifact_${name}`;
	const tsFilepath = path.join(folder, 'artifacts', canonicalName) + '.ts';
	const folderPath = path.dirname(tsFilepath);
	ensureDirExistsSync(folderPath);
	if (mode === 'typescript') {
		const newContent = `export const ${artifactName}: ${JSON.stringify(data, null, 2)} = ${JSON.stringify(data, null, 2)} as const;`;
		writeIfDifferent(tsFilepath, newContent);
	} else if (mode === 'javascript') {
		const newContent = `export const ${artifactName} = /** @type {const} **/ (${JSON.stringify(data, null, 2)});`;
		const dtsContent = `export declare const ${artifactName}: ${JSON.stringify(data, null, 2)};`;
		const jsFilepath = path.join(folder, 'artifacts', canonicalName) + '.js';
		writeIfDifferent(jsFilepath, newContent);
		writeIfDifferent(jsFilepath.replace(/\.js$/, '.d.ts'), dtsContent);
	}
}

function writeArtifactIndexToFile(folder: string, data: Artifacts, mode: 'typescript' | 'javascript') {
	const tsFilepath = path.join(folder, 'artifacts', 'index') + '.ts';
	const folderPath = path.dirname(tsFilepath);
	ensureDirExistsSync(folderPath);
	if (mode === 'typescript') {
		let newContent = '';
		for (const canonicalName of Object.keys(data)) {
			const transformedName = canonicalName.replace(/[\/.\-]/g, '_');
			const name = canonicalName.split('/').pop();
			const artifactName = `Artifact_${name}`;
			const importNaming =
				canonicalName != name ? `${artifactName} as ${transformedName}` : `${artifactName} as ${name}`;
			newContent += `export {${importNaming}} from './${canonicalName}.js';\n`;
		}

		writeIfDifferent(tsFilepath, newContent);
	} else if (mode === 'javascript') {
		let newContent = '';
		for (const canonicalName of Object.keys(data)) {
			const transformedName = canonicalName.replace(/[\/.\-]/g, '_');
			const name = canonicalName.split('/').pop();
			const artifactName = `Artifact_${name}`;
			const importNaming =
				canonicalName != name ? `${artifactName} as ${transformedName}` : `${artifactName} as ${name}`;
			newContent += `export {${importNaming}} from './${canonicalName}.js';\n`;
		}
		const jsFilepath = path.join(folder, 'artifacts', 'index') + '.js';
		writeIfDifferent(jsFilepath, newContent);
		writeIfDifferent(jsFilepath.replace(/\.js$/, '.d.ts'), newContent);
	}
}

function writeABIDefinitionToFile(
	folder: string,
	canonicalName: string,
	data: Artifact,
	mode: 'typescript' | 'javascript',
) {
	const nameAsPath = canonicalName.split('/');
	const name = nameAsPath[nameAsPath.length - 1];
	const abiName = `Abi_${name}`;
	const artifactName = `Artifact_${name}`;
	const relativePath = `../`.repeat(nameAsPath.length);
	const tsFilepath = path.join(folder, 'abis', canonicalName) + '.ts';
	const folderPath = path.dirname(tsFilepath);
	ensureDirExistsSync(folderPath);

	if (REF_ARTIFACT_ABI) {
		if (mode === 'typescript') {
			const newContent = `import {${artifactName}} from '${relativePath}artifacts/${canonicalName}.js';
export type ${abiName} = (typeof ${artifactName})['abi'];\n
export const ${abiName}: ${abiName} = ${artifactName}['abi'];\n`;
			writeIfDifferent(tsFilepath, newContent);
		} else if (mode === 'javascript') {
			const jsFilepath = path.join(folder, 'abis', canonicalName) + '.js';
			const newContent = `export {};\n`;
			const dtsContent = `import {${artifactName}} from '${relativePath}artifacts/${canonicalName}.js';
export type ${abiName} = (typeof ${artifactName})['abi'];\n
export const ${abiName} = ${artifactName}['abi'];\n`;

			writeIfDifferent(jsFilepath, newContent);
			writeIfDifferent(jsFilepath.replace(/\.js$/, '.d.ts'), dtsContent);
		}
	} else {
		if (mode === 'typescript') {
			const newContent = `export type ${abiName} = ${JSON.stringify(data.abi, null, 2)};
export const ${abiName}: ${abiName} = ${JSON.stringify(data.abi, null, 2)};\n`;
			writeIfDifferent(tsFilepath, newContent);
		} else if (mode === 'javascript') {
			const jsFilepath = path.join(folder, 'abis', canonicalName) + '.js';
			const newContent = `export const ${abiName} = /** @type {const} **/ (${JSON.stringify(data.abi, null, 2)});`;
			const dtsContent = `export type ${abiName} = ${JSON.stringify(data.abi, null, 2)};
export declare const ${abiName}: ${abiName};\n`;
			writeIfDifferent(jsFilepath, newContent);
			writeIfDifferent(jsFilepath.replace(/\.js$/, '.d.ts'), dtsContent);
		}
	}
}
function writeABIDefinitionIndexToFile(folder: string, data: Artifacts, mode: 'typescript' | 'javascript') {
	const tsFilepath = path.join(folder, 'abis', 'index') + '.ts';
	const folderPath = path.dirname(tsFilepath);
	ensureDirExistsSync(folderPath);
	if (mode === 'typescript') {
		let newContent = '';
		for (const canonicalName of Object.keys(data)) {
			const transformedName = canonicalName.replace(/[\/.\-]/g, '_');
			const name = canonicalName.split('/').pop();
			const abiName = `Abi_${name}`;
			const importNaming = canonicalName != name ? `${abiName} as ${transformedName}` : `${abiName} as ${name}`;
			newContent += `export {${importNaming}} from "./${canonicalName}.js"\n`;
		}
		writeIfDifferent(tsFilepath, newContent);
	} else if (mode === 'javascript') {
		const jsFilepath = path.join(folder, 'abis', 'index') + '.js';
		let newContent = '';
		for (const canonicalName of Object.keys(data)) {
			const transformedName = canonicalName.replace(/[\/.\-]/g, '_');
			const name = canonicalName.split('/').pop();
			const abiName = `Abi_${name}`;
			const importNaming = canonicalName != name ? `${abiName} as ${transformedName}` : `${abiName} as ${name}`;
			newContent += `export {${importNaming}} from "./${canonicalName}.js"\n`;
		}
		writeIfDifferent(jsFilepath, newContent);
		writeIfDifferent(jsFilepath.replace(/\.js$/, '.d.ts'), newContent);
	}
}

export async function generateTypes(paths: {artifacts: string[]}, config: ArtifactGenerationConfig): Promise<void> {
	const buildInfoCache = new Map<string, any>();
	const allArtifacts: {[name: string]: any} = {};
	const artifactsWithBytecode: {[name: string]: any} = {};
	const shortNameDict: {[shortName: string]: boolean} = {};

	for (const artifactsPath of paths.artifacts) {
		const files: FileTraversed[] = traverse(
			artifactsPath,
			[],
			artifactsPath,
			(name) => name != 'build-info' && !name.endsWith('.t.sol') && !name.endsWith('.dbg.json'),
		);

		// console.log('--------------------------');
		// console.log(files);
		// console.log('--------------------------');

		for (const file of files) {
			const filepath = file.path;
			if (file.directory || !filepath.endsWith('.json')) {
				continue;
			}
			const filename = slash(path.basename(filepath));
			const dirname = slash(path.dirname(file.relativePath));

			// const namePath = dirname.replace('.sol', '');
			const contractName = filename.replace('.json', '');
			// const shortName = artifact.artifactsEmitted[i];
			// console.log({path: filepath});
			const content = fs.readFileSync(filepath, 'utf-8');
			const parsed = JSON.parse(content);

			if (!parsed.buildInfoId) continue;

			// TODO read config for artifacts folder
			let buildInfoFilepath = path.join(artifactsPath, 'build-info', `${parsed.buildInfoId}.output.json`);

			if (!parsed.buildInfoId) {
				// support hardhat v2 artifacts files
				if (fs.existsSync(filepath.replace('.json', '.dbg.json'))) {
					// console.warn(`Artifact ${filepath} does not have a buildInfoId, but found a .dbg.json file. Using that instead.`);
					const dbgContent = fs.readFileSync(filepath.replace('.json', '.dbg.json'), 'utf-8');
					const dbgParsed = JSON.parse(dbgContent);
					const buildInfoRelativePath = dbgParsed.buildInfo;
					parsed.buildInfoId = path.basename(buildInfoRelativePath, '.json');
					// console.log({buildInfoRelativePath, buildInfoId: parsed.buildInfoId});
					buildInfoFilepath = path.join(artifactsPath, 'build-info', `${parsed.buildInfoId}.json`);
				}
			}

			// const backupBuildInfoFilepath = path.join(
			// 	'./generated',
			// 	buildInfoFilepath.slice(buildInfoFilepath.indexOf('/', 1))
			// );
			let buildInfoFilepathToUse = buildInfoFilepath;
			// if (!fs.existsSync(buildInfoFilepathToUse)) {
			// 	buildInfoFilepathToUse = backupBuildInfoFilepath;
			// }
			let parsedBuildInfo;
			if (!buildInfoCache.has(buildInfoFilepathToUse)) {
				if (!fs.existsSync(buildInfoFilepathToUse)) continue;
				const buildInfoContent = fs.readFileSync(buildInfoFilepathToUse, 'utf-8');
				parsedBuildInfo = JSON.parse(buildInfoContent);
				buildInfoCache.set(buildInfoFilepathToUse, parsedBuildInfo);
			} else {
				parsedBuildInfo = buildInfoCache.get(buildInfoFilepathToUse);
			}

			const solidityOutput = parsedBuildInfo.output.contracts[parsed.inputSourceName][contractName];
			const hardhatArtifactObject = {...parsed, ...solidityOutput};
			const {buildInfoId, _format, ...artifactObject} = hardhatArtifactObject;

			const hasBytecode = artifactObject.bytecode && artifactObject.bytecode !== '0x';

			const fullName = `${dirname}/${contractName}`;
			allArtifacts[fullName] = artifactObject;

			if (hasBytecode) {
				artifactsWithBytecode[fullName] = artifactObject;
			}
			if (shortNameDict[contractName]) {
				delete allArtifacts[contractName];
				delete artifactsWithBytecode[contractName];
			} else {
				allArtifacts[contractName] = artifactObject;
				if (hasBytecode) {
					artifactsWithBytecode[contractName] = artifactObject;
				}
				shortNameDict[contractName] = true;
			}
		}
	}

	for (const key of Object.keys(allArtifacts)) {
		if (key.indexOf('/') >= 0) {
			const split = key.split('/');
			if (split.length > 1) {
				const shortName = split[split.length - 1];
				if (allArtifacts[shortName]) {
					delete allArtifacts[key];
					delete artifactsWithBytecode[key];
				}
			}
		}
	}

	// for (const key of Object.keys(allArtifacts)) {
	// 	const artifact = allArtifacts[key];
	// 	writeFiles(key, artifact, config);
	// }
	// // const json = hre.config.generateTypedArtifacts.json || [];
	// // json.push('./generated/_artifacts.json');
	// // writeFiles(undefined, allArtifacts, {...hre.config.generateTypedArtifacts, json: json});

	// writeFiles(undefined, allArtifacts, config);

	for (const destination of config.destinations) {
		const generatedFolder = destination.folder;
		const mode = destination.mode;
		for (const key of Object.keys(allArtifacts)) {
			writeABIDefinitionToFile(generatedFolder, key, allArtifacts[key], mode);
			const artifactWithBytecode = artifactsWithBytecode[key];
			if (artifactWithBytecode || GENERATE_EMPTY_ARTIFACTS) {
				writeArtifactToFile(generatedFolder, key, allArtifacts[key], mode);
			}
		}

		writeArtifactIndexToFile(generatedFolder, GENERATE_EMPTY_ARTIFACTS ? allArtifacts : artifactsWithBytecode, mode);
		writeABIDefinitionIndexToFile(generatedFolder, allArtifacts, mode);
	}
}
