import {existsSync} from 'fs';
import {resolve} from 'path';
import {type Configuration as WebpackConfig} from 'webpack';
import type {Configuration as DevServerConfig} from 'webpack-dev-server';
import type {BabelConfig} from '../config/babel.config';
import type {JestConfig} from '../config/jest.config.js';
import {getPackageConfig} from '@lipemat/js-boilerplate-shared/helpers/package-config.js';
import type {EntriesConfig} from '../config/entries.config.js';
import type {CssLoaderConfig} from '../config/css-loader.config.js';
import {ensureJSExtension, getExtensionsConfig, mergeWithLocalConfig} from '@lipemat/js-boilerplate-shared/helpers/config.js';

type Configs = {
	'babel.config.js': BabelConfig;
	'css-loader.config.js': CssLoaderConfig;
	'dev-server.config.js': DevServerConfig;
	'entries.config.js': EntriesConfig;
	'jest.config.js': JestConfig;
	'webpack.dev.js': WebpackConfig;
	'webpack.dist.js': WebpackConfig;
};

const {workingDirectory, packageDirectory} = getPackageConfig();


/**
 * Check to see if a local config file exists.
 *
 * @param {string}  fileName
 * @param {boolean} inWorkingDirectory - Look in working directory instead of their /config directory
 *
 * @return {boolean}
 */
export async function hasLocalOverride( fileName: string, inWorkingDirectory: boolean = false ): Promise<boolean> {
	let hasLocal = false;
	try {
		if ( inWorkingDirectory ) {
			await import( resolve( workingDirectory, fileName ) );
			hasLocal = true;
		} else {
			await import( resolve( packageDirectory + '/config', fileName ) );
			hasLocal = true;
		}
	} catch ( e ) {
		if ( e instanceof Error ) {
			if ( ! ( 'code' in e ) || 'MODULE_NOT_FOUND' !== e.code ) {
				console.error( e );
			}
		}
	}

	return hasLocal;
}


/**
 * Get a config from our /config directory merged with any
 * matching configuration from the project directory.
 *
 * For instance, if we have a file named config/babel.config.js in our project,
 * we will merge the contents with our config/babel.config.js in favor of whatever
 * is specified with the project's file.
 *
 * If the `module.exports` is a function, the existing configuration will be passed
 * as the only argument. Otherwise, standard `module.exports` are also supported.
 *
 * @example ```ts
 * // standard
 * module.export = {
 *     externals: {extra: 'Extra'}
 * }
 * // function
 * module.exports = function(config) {
 *     return {
 *         externals: {...config.externals, extra: 'Extra'}
 *     }
 * }
 * ```
 *
 * @param {string} fileName
 *
 * @return {Object}
 */
export async function getConfig<T extends keyof Configs>( fileName: T ): Promise<Configs[T]> {
	const configModule = await import( ensureJSExtension( `../config/${fileName}` ) );
	const config = configModule.default;

	const extensionsConfig = getExtensionsConfig<Configs[T]>( fileName, config );
	const mergedConfig: Configs[T] = {...config, ...extensionsConfig};

	return mergeWithLocalConfig<Configs[T]>( fileName, mergedConfig );
}


/**
 * Get the path to the "tsconfig.json" file if it exists.
 *
 * 1. The working directory.
 * 2. The package directory.
 *
 * The package directory takes priority over the working directory.
 *
 *
 * @return {string}
 */
export function getTsConfigFile(): string {
	const possibles = [
		// Backward compatible for before @lipemat/eslint-config version 3.
		resolve( workingDirectory + '/tsconfig.json' ),
		resolve( packageDirectory + '/tsconfig.json' ),
	].filter( existsSync );

	let tsConfig = '';
	possibles.forEach( filePath => {
		tsConfig = filePath;
	} );
	return tsConfig;
}
