import { extname, relative, resolve } from 'path';
import { ContextExclusionPlugin, HotModuleReplacementPlugin } from 'webpack';
import Config from 'webpack-chain';
import { satisfies } from 'semver';
import { isVersionGteConsideringPrerelease } from '../helpers/dependencies';
import { existsSync } from 'fs';

import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import TerserPlugin from 'terser-webpack-plugin';

import { getProjectFilePath, getProjectTSConfigPath } from '../helpers/project';
import {
	getDependencyVersion,
	hasDependency,
	getResolvedDependencyVersionForCheck,
} from '../helpers/dependencies';
import { PlatformSuffixPlugin } from '../plugins/PlatformSuffixPlugin';
import { applyFileReplacements } from '../helpers/fileReplacements';
import { addCopyRule, applyCopyRules } from '../helpers/copyRules';
import { WatchStatePlugin } from '../plugins/WatchStatePlugin';
import { CompatDefinePlugin } from '../plugins/CompatDefinePlugin';
import { applyDotEnvPlugin } from '../helpers/dotEnv';
import { env as _env, IWebpackEnv } from '../index';
import { getValue } from '../helpers/config';
import { getIPS } from '../helpers/host';
import FixSourceMapUrlPlugin from '../plugins/FixSourceMapUrlPlugin';
import {
	getAvailablePlatforms,
	getAbsoluteDistPath,
	getPlatformName,
	getEntryDirPath,
	getEntryPath,
} from '../helpers/platform';

export default function (config: Config, env: IWebpackEnv = _env): Config {
	const entryPath = getEntryPath();
	const platform = getPlatformName();
	const outputPath = getAbsoluteDistPath();
	const mode = env.production ? 'production' : 'development';

	// set mode
	config.mode(mode);

	// use source map files by default with v9+
	function useSourceMapFiles() {
		if (mode === 'development') {
			// in development we always use source-map files with v9+ runtimes
			// they are parsed and mapped to display in-flight app error screens
			env.sourceMap = 'source-map';
		}
	}
	// determine target output by @nativescript/* runtime version
	// v9+ supports ESM output, anything below uses CommonJS
	if (
		hasDependency('@nativescript/ios') ||
		hasDependency('@nativescript/visionos') ||
		hasDependency('@nativescript/android')
	) {
		const iosVersion = getDependencyVersion('@nativescript/ios');
		const visionosVersion = getDependencyVersion('@nativescript/visionos');
		const androidVersion = getDependencyVersion('@nativescript/android');

		if (platform === 'ios') {
			const iosResolved =
				getResolvedDependencyVersionForCheck('@nativescript/ios', '9.0.0') ??
				iosVersion ??
				undefined;
			if (isVersionGteConsideringPrerelease(iosResolved, '9.0.0')) {
				useSourceMapFiles();
			} else {
				env.commonjs = true;
			}
		} else if (platform === 'visionos') {
			const visionosResolved =
				getResolvedDependencyVersionForCheck(
					'@nativescript/visionos',
					'9.0.0',
				) ??
				visionosVersion ??
				undefined;
			if (isVersionGteConsideringPrerelease(visionosResolved, '9.0.0')) {
				useSourceMapFiles();
			} else {
				env.commonjs = true;
			}
		} else if (platform === 'android') {
			const androidResolved =
				getResolvedDependencyVersionForCheck(
					'@nativescript/android',
					'9.0.0',
				) ??
				androidVersion ??
				undefined;
			if (isVersionGteConsideringPrerelease(androidResolved, '9.0.0')) {
				useSourceMapFiles();
			} else {
				env.commonjs = true;
			}
		}
	} else {
		env.commonjs = true;
	}

	if (env.hmr) {
		// HMR webpack should use CommonJS
		env.commonjs = true;
	}

	// config.stats({
	// 	logging: 'verbose'
	// })

	// package.json is generated by the CLI with runtime options
	// this ensures it's not included in the bundle, but rather
	// resolved at runtime
	config.externals(['package.json', '~/package.json']);

	// disable marking built-in node modules as external
	// since they are not available at runtime and
	// should be bundled (requires polyfills)
	// for example `npm i --save url` to
	// polyfill the node url module.
	config.set('externalsPresets', {
		node: false,
	});

	// Mock Node.js built-ins that are not available in NativeScript runtime
	// but are required by some packages like css-tree
	config.resolve.merge({
		fallback: {
			module: require.resolve('../polyfills/module.js'),
		},
		alias: {
			// Mock mdn-data modules that css-tree tries to load
			'mdn-data/css/properties.json': require.resolve(
				'../polyfills/mdn-data-properties.js',
			),
			'mdn-data/css/syntaxes.json': require.resolve(
				'../polyfills/mdn-data-syntaxes.js',
			),
			'mdn-data/css/at-rules.json': require.resolve(
				'../polyfills/mdn-data-at-rules.js',
			),
			// Ensure imports of the Node 'module' builtin resolve to our polyfill
			module: require.resolve('../polyfills/module.js'),
		},
		// Allow extension-less ESM imports (fixes "fully specified" errors)
		// Example: '../timer' -> resolves to index.<platform>.js without requiring explicit extension
		fullySpecified: false,
	});

	// As an extra guard, ensure rule-level resolve also allows extension-less imports
	config.module
		.rule('esm-extensionless')
		.test(/\.(mjs|js|ts|tsx)$/)
		.resolve.set('fullySpecified', false);

	const getSourceMapType = (map: string | boolean): Config.DevTool => {
		const defaultSourceMap = 'inline-source-map';

		if (typeof map === 'undefined') {
			// source-maps disabled in production by default
			// enabled with --env.sourceMap=<type>
			if (mode === 'production') {
				// todo: we may set up SourceMapDevToolPlugin to generate external maps in production
				return false;
			}

			return defaultSourceMap;
		}

		// when --env.sourceMap=true is passed, use default
		if (typeof map === 'boolean' && map) {
			return defaultSourceMap;
		}

		// pass any type of sourceMap with --env.sourceMap=<type>
		return map as Config.DevTool;
	};

	const sourceMapType = getSourceMapType(env.sourceMap);

	// Use devtool for both CommonJS and ESM - let webpack handle source mapping properly
	config.devtool(sourceMapType);

	// For ESM builds, fix the sourceMappingURL to use correct paths
	if (sourceMapType && sourceMapType !== 'hidden-source-map') {
		config
			.plugin('FixSourceMapUrlPlugin')
			.use(FixSourceMapUrlPlugin as any, [{ outputPath }]);
	}

	// when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder
	if (env.sourceMap === 'hidden-source-map') {
		const sourceMapAbsolutePath = getProjectFilePath(
			`./${
				env.buildPath ?? 'platforms'
			}/${platform}-sourceMaps/[file].map[query]`,
		);
		const sourceMapRelativePath = relative(outputPath, sourceMapAbsolutePath);
		config.output.sourceMapFilename(sourceMapRelativePath);
	}

	// todo: figure out easiest way to make "node" target work in ns
	// rather than the custom ns target implementation that's hard to maintain
	// appears to be working - but we still have to deal with HMR
	config.target('node');

	// config.entry('globals').add('@nativescript/core/globals/index').end();

	config
		.entry('bundle')
		// ensure we load nativescript globals first
		.add('@nativescript/core/globals/index')
		.add('@nativescript/core/bundle-entry-points')
		.add(entryPath);

	// Add android app components to the bundle to SBG can generate the java classes
	if (platform === 'android') {
		const appComponents = Array.isArray(env.appComponents)
			? env.appComponents
			: (env.appComponents && [env.appComponents]) || [];
		appComponents.push('@nativescript/core/ui/frame');
		appComponents.push('@nativescript/core/ui/frame/activity');
		appComponents.map((component) => {
			config.entry('bundle').add(component);
		});
	}

	// inspector_modules
	config.when(shouldIncludeInspectorModules(), (config) => {
		config
			.entry('tns_modules/inspector_modules')
			.add('@nativescript/core/inspector_modules');
	});

	if (env.commonjs) {
		// CommonJS output
		config.output
			.path(outputPath)
			.pathinfo(false)
			.publicPath('')
			.libraryTarget('commonjs')
			.globalObject('global')
			.set('clean', true);
		if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
			config.output.filename(`[name].${env.uniqueBundle}.js`);
		}
	} else {
		// ESM output
		config.merge({
			experiments: {
				// enable ES module syntax (import/exports)
				outputModule: true,
			},
		});

		config.output
			.path(outputPath)
			.pathinfo(false)
			.publicPath('file:///app/')
			.set('module', true)
			.libraryTarget('module')
			.globalObject('global')
			.set('clean', true);
		if (env === null || env === void 0 ? void 0 : env.uniqueBundle) {
			config.output.filename(`[name].${env.uniqueBundle}.mjs`);
		}
	}

	config.watchOptions({
		ignored: [
			`${getProjectFilePath(env.buildPath ?? 'platforms')}/**`,
			`${getProjectFilePath(env.appResourcesPath ?? 'App_Resources')}/**`,
		],
	});

	// allow watching node_modules
	config.when(env.watchNodeModules, (config) => {
		config.set('snapshot', {
			managedPaths: [],
		});
	});

	// Set up Terser options
	config.optimization.minimizer('TerserPlugin').use(TerserPlugin, [
		{
			terserOptions: {
				// @ts-ignore - https://github.com/webpack-contrib/terser-webpack-plugin/pull/463 broke the types?
				compress: {
					collapse_vars: platform !== 'android',
					sequences: platform !== 'android',
					keep_infinity: true,
					drop_console: mode === 'production',
					global_defs: {
						__UGLIFIED__: true,
					},
				},
				keep_fnames: true,
				keep_classnames: true,
				format: {
					keep_quoted_props: true,
				},
			},
		},
	]);

	config.optimization.runtimeChunk('single');

	if (env.commonjs) {
		// Set up CommonJS output
		config.optimization.splitChunks({
			cacheGroups: {
				defaultVendor: {
					test: /[\\/]node_modules[\\/]/,
					priority: -10,
					name: 'vendor',
					chunks: 'all',
				},
			},
		});
	} else {
		// Set up ESM output
		config.output.chunkFilename('[name].mjs');

		// now re‑add exactly what you want:
		config.optimization.splitChunks({
			// only split out vendor from the main bundle…
			chunks: 'initial',
			cacheGroups: {
				// no “default” group
				default: false,

				// only pull node_modules into vendor.js from the *initial* chunk
				vendor: {
					test: /[\\/]node_modules[\\/]/,
					name: 'vendor',
					chunks: 'initial',
					priority: -10,
					reuseExistingChunk: true,
				},
			},
		});

		config.optimization.set('moduleIds', 'named').set('chunkIds', 'named');
	}

	// look for loaders in
	//  - node_modules/@nativescript/webpack/dist/loaders
	//  - node_modules/@nativescript/webpack/node_modules
	//  - node_modules
	// allows for cleaner rules, without having to specify full paths to loaders
	config.resolveLoader.modules
		.add(resolve(__dirname, '../loaders'))
		.add(resolve(__dirname, '../../node_modules'))
		.add(getProjectFilePath('node_modules'))
		.add('node_modules');

	config.resolve.extensions
		.add(`.${platform}.ts`)
		.add('.ts')
		.add(`.${platform}.js`)
		.add('.js')
		.add(`.${platform}.mjs`)
		.add('.mjs')
		.add(`.${platform}.css`)
		.add('.css')
		.add(`.${platform}.scss`)
		.add('.scss')
		.add(`.${platform}.json`)
		.add('.json');

	if (platform === 'visionos') {
		// visionOS allows for both .ios and .visionos extensions
		const extensions = config.resolve.extensions.values();
		const newExtensions = [];
		extensions.forEach((ext) => {
			newExtensions.push(ext);
			if (ext.includes('visionos')) {
				newExtensions.push(ext.replace('visionos', 'ios'));
			}
		});

		config.resolve.extensions.clear().merge(newExtensions);
	}

	// base aliases
	config.resolve.alias.set('~', getEntryDirPath()).set('@', getEntryDirPath());

	// resolve symlinks
	config.resolve.symlinks(true);

	// resolve modules in project node_modules first
	// then fall-back to default node resolution (up the parent folder chain)
	config.resolve.modules
		.add(getProjectFilePath('node_modules'))
		.add('node_modules');

	config.module
		.rule('bundle')
		.enforce('post')
		.test(entryPath)
		.use('app-css-loader')
		.loader('app-css-loader')
		.options({
			// TODO: allow both visionos and ios to resolve for css
			// only resolve .ios css on visionOS for now
			// platform: platform === 'visionos' ? 'ios' : platform,
			platform,
		})
		.end();

	config.when(env.hmr, (config) => {
		config.module
			.rule('bundle')
			.use('nativescript-hot-loader')
			.loader('nativescript-hot-loader')
			.options({
				injectHMRRuntime: true,
			});
	});

	// enable profiling with --env.profile
	config.when(env.profile, (config) => {
		config.profile(true);
	});

	// worker-loader should be declared before ts-loader
	config.module
		.rule('workers')
		.test(/\.(mjs|js|ts)$/)
		.use('nativescript-worker-loader')
		.loader('nativescript-worker-loader');

	const tsConfigPath = getProjectTSConfigPath();
	const configFile = tsConfigPath
		? {
				configFile: tsConfigPath,
			}
		: undefined;

	// set up ts support
	config.module
		.rule('ts')
		.test([/\.ts$/])
		.use('ts-loader')
		.loader('ts-loader')
		.options({
			// todo: perhaps we can provide a default tsconfig
			// and use that if the project doesn't have one?
			...configFile,
			transpileOnly: true,
			allowTsInNodeModules: true,
			compilerOptions: {
				sourceMap: true,
				declaration: false,
			},
			getCustomTransformers() {
				return {
					before: [require('../transformers/NativeClass').default],
				};
			},
		})
		.end()
		// Ensure pre-loaders run BEFORE ts-loader (loaders execute right-to-left):
		// order: [ts-loader, native-class-downlevel-loader, native-class-strip-loader]
		// execution: strip -> downlevel -> ts-loader
		.use('native-class-downlevel-loader')
		.loader('native-class-downlevel-loader')
		.end()
		.use('native-class-strip-loader')
		.loader('native-class-strip-loader');

	// Use Fork TS Checker to do type checking in a separate non-blocking process
	config.when(hasDependency('typescript'), (config) => {
		config
			.plugin('ForkTsCheckerWebpackPlugin')
			.use(ForkTsCheckerWebpackPlugin, [
				{
					async: !!env.watch,
					typescript: {
						memoryLimit: 4096,
						...configFile,
					},
				},
			]);
	});

	// set up js
	config.module
		.rule('js')
		.test(/\.js$/)
		.exclude.add(/node_modules/)
		.end();

	// config.resolve.extensions.add('.xml');
	// set up xml
	config.module
		.rule('xml')
		.test(/\.xml$/)
		.use('xml-namespace-loader')
		.loader('xml-namespace-loader');

	// default PostCSS options to use
	// projects can change settings
	// via postcss.config.js
	const postCSSOptions = {
		postcssOptions: {
			plugins: [
				// inlines @imported stylesheets
				[
					'postcss-import',
					{
						// custom resolver to resolve platform extensions in @import statements
						// ie. @import "foo.css" would import "foo.ios.css" if the platform is ios and it exists
						resolve(id, baseDir, importOptions) {
							const extensions =
								platform === 'visionos' ? [platform, 'ios'] : [platform];
							for (const platformTarget of extensions) {
								const ext = extname(id);
								const platformExt = ext ? `.${platformTarget}${ext}` : '';

								if (!id.includes(platformExt)) {
									const platformRequest = id.replace(ext, platformExt);
									const extPath = resolve(baseDir, platformRequest);

									try {
										return require.resolve(platformRequest, {
											paths: [baseDir],
										});
									} catch {}

									if (existsSync(extPath)) {
										console.log(`resolving "${id}" to "${platformRequest}"`);
										return extPath;
									}
								}
							}

							// fallback to postcss-import default resolution
							return id;
						},
					},
				],
			],
		},
	};

	// set up css
	config.module
		.rule('css')
		.test(/\.css$/)
		.use('apply-css-loader')
		.loader('apply-css-loader')
		.end()
		.use('css2json-loader')
		.loader('css2json-loader')
		.end()
		.use('postcss-loader')
		.loader('postcss-loader')
		.options(postCSSOptions);

	// set up scss
	config.module
		.rule('scss')
		.test(/\.scss$/)
		.use('apply-css-loader')
		.loader('apply-css-loader')
		.end()
		.use('css2json-loader')
		.loader('css2json-loader')
		.end()
		.use('postcss-loader')
		.loader('postcss-loader')
		.options(postCSSOptions)
		.end()
		.use('sass-loader')
		.loader('sass-loader')
		.options({
			// helps ensure proper project compatibility
			// particularly in cases of workspaces
			// which may have different nested Sass implementations
			// via transient dependencies
			implementation: require('sass'),
		});

	// config.plugin('NormalModuleReplacementPlugin').use(NormalModuleReplacementPlugin, [
	// 	/.*/,
	// 	request => {
	// 		if (new RegExp(`\.${platform}\..+$`).test(request.request)) {
	// 			request.rawRequest = request.rawRequest.replace(`.${platform}.`, '.')
	// 			console.log(request)
	// 		}
	// 	}
	// ])

	config.plugin('PlatformSuffixPlugin').use(PlatformSuffixPlugin, [
		{
			extensions: platform === 'visionos' ? [platform, 'ios'] : [platform],
		},
	]);

	// Makes sure that require.context will never include
	// App_Resources, regardless where they are located.
	config
		.plugin('ContextExclusionPlugin|App_Resources')
		.use(ContextExclusionPlugin, [new RegExp(`(.*)App_Resources(.*)`)]);

	// Makes sure that require.context will never include code from
	// another platform (ie .android.ts when building for ios)
	const otherPlatformsRE = getAvailablePlatforms()
		.filter((platform) => platform !== getPlatformName())
		.join('|');

	config
		.plugin('ContextExclusionPlugin|Other_Platforms')
		.use(ContextExclusionPlugin, [
			new RegExp(`\.(${otherPlatformsRE})\.(\w+)$`),
		]);

	// Filter common undesirable warnings
	config.set(
		'ignoreWarnings',
		(config.get('ignoreWarnings') ?? []).concat([
			/**
			 * This rule hides
			 * +-----------------------------------------------------------------------------------------+
			 * | WARNING in ./node_modules/@angular/core/fesm2015/core.js 29714:15-102                   |
			 * | System.import() is deprecated and will be removed soon. Use import() instead.           |
			 * | For more info visit https://webpack.js.org/guides/code-splitting/                       |
			 * +-----------------------------------------------------------------------------------------+
			 */
			/System.import\(\) is deprecated/,
		]),
	);

	// todo: refine defaults
	config.plugin('DefinePlugin').use(
		CompatDefinePlugin as any,
		[
			{
				__DEV__: mode === 'development',
				__NS_WEBPACK__: true,
				__NS_ENV_VERBOSE__: !!env.verbose,
				__NS_DEV_HOST_IPS__:
					mode === 'development' ? JSON.stringify(getIPS()) : `[]`,
				__CSS_PARSER__: JSON.stringify(getValue('cssParser', 'css-tree')),
				__UI_USE_XML_PARSER__: true,
				__UI_USE_EXTERNAL_RENDERER__: false,
				__COMMONJS__: !!env.commonjs,
				__ANDROID__: platform === 'android',
				__IOS__: platform === 'ios',
				__VISIONOS__: platform === 'visionos',
				__APPLE__: platform === 'ios' || platform === 'visionos',
				/* for compat only */ 'global.isAndroid': platform === 'android',
				/* for compat only */ 'global.isIOS':
					platform === 'ios' || platform === 'visionos',
				/* for compat only */ 'global.isVisionOS': platform === 'visionos',
				process: 'global.process',
			},
		] as any,
	);

	// enable DotEnv
	applyDotEnvPlugin(config);

	// replacements
	applyFileReplacements(config);

	// set up default copy rules
	addCopyRule('assets/**');
	addCopyRule('fonts/**');
	addCopyRule('**/*.+(jpg|png)');

	applyCopyRules(config);

	config.plugin('WatchStatePlugin').use(WatchStatePlugin);

	config.when(env.hmr, (config) => {
		config.plugin('HotModuleReplacementPlugin').use(HotModuleReplacementPlugin);
	});

	config.when(env.report, (config) => {
		config.plugin('BundleAnalyzerPlugin').use(BundleAnalyzerPlugin, [
			{
				analyzerMode: 'static',
				generateStatsFile: true,
				openAnalyzer: false,
				reportFilename: getProjectFilePath('report/report.html'),
				statsFilename: getProjectFilePath('report/stats.json'),
			},
		]);
	});

	return config;
}

function shouldIncludeInspectorModules(): boolean {
	const platform = getPlatformName();
	const coreVersion = getDependencyVersion('@nativescript/core');

	if (coreVersion && satisfies(coreVersion, '>=8.7.0')) {
		return platform === 'ios' || platform === 'android';
	}

	return platform === 'ios';
}
