/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {execSync} from 'node:child_process';
import fs from 'node:fs';
import path, {dirname} from 'node:path';
import {fileURLToPath} from 'node:url';
import {CopyRspackPlugin, DefinePlugin, HtmlRspackPlugin, SwcJsMinimizerRspackPlugin} from '@rspack/core';
import {createPoFileRule, getLinguiSwcPluginConfig} from './scripts/build/rspack/lingui.mjs';
import {staticFilesPlugin} from './scripts/build/rspack/static-files.mjs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const ROOT_DIR = path.resolve(__dirname, '.');
const MONOREPO_ROOT = path.resolve(__dirname, '..');
const SRC_DIR = path.join(ROOT_DIR, 'src');
const DIST_DIR = path.join(ROOT_DIR, 'dist');
const PKGS_DIR = path.join(ROOT_DIR, 'pkgs');
const PUBLIC_DIR = path.join(ROOT_DIR, 'assets');

const CDN_ENDPOINT = 'https://fluxerstatic.com';

function resolveMode() {
	const modeIndex = process.argv.indexOf('--mode');
	if (modeIndex >= 0) {
		const modeValue = process.argv[modeIndex + 1];
		if (modeValue) {
			return modeValue;
		}
	}
	return 'production';
}

const mode = resolveMode();
const isProduction = mode === 'production';
const isDevelopment = !isProduction;
const devJsName = 'assets/[name].js';
const devCssName = 'assets/[name].css';

function isPlainObject(value) {
	return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function readConfig() {
	const configPath = process.env.FLUXER_CONFIG;
	if (!configPath) {
		throw new Error('FLUXER_CONFIG must be set to a JSON config path.');
	}
	const resolvedConfigPath = path.isAbsolute(configPath) ? configPath : path.resolve(MONOREPO_ROOT, configPath);
	const content = fs.readFileSync(resolvedConfigPath, 'utf-8');
	const parsed = JSON.parse(content);
	if (!isPlainObject(parsed)) {
		throw new Error('Invalid JSON config: expected an object at root.');
	}
	return parsed;
}

function getValue(source, path, fallback) {
	let current = source;
	for (const segment of path) {
		if (!isPlainObject(current)) {
			return fallback;
		}
		current = current[segment];
	}
	return current ?? fallback;
}

function asString(value, fallback = undefined) {
	if (value === null || value === undefined) {
		return fallback;
	}
	if (typeof value === 'string') {
		return value;
	}
	if (typeof value === 'number' || typeof value === 'boolean') {
		return String(value);
	}
	return fallback;
}

function normalizeOverride(value) {
	const trimmed = asString(value, '')?.trim() ?? '';
	return trimmed === '' ? undefined : trimmed;
}

function buildUrl(scheme, domain, port, path = '') {
	const isStandardPort =
		(scheme === 'http' && port === 80) ||
		(scheme === 'https' && port === 443) ||
		(scheme === 'ws' && port === 80) ||
		(scheme === 'wss' && port === 443);
	const portPart = port && !isStandardPort ? `:${port}` : '';
	return `${scheme}://${domain}${portPart}${path}`;
}

function deriveDomain(endpointType, config) {
	switch (endpointType) {
		case 'staticCdn':
			return config.static_cdn_domain || config.base_domain;
		case 'invite':
			return config.invite_domain || config.base_domain;
		case 'gift':
			return config.gift_domain || config.base_domain;
		default:
			return config.base_domain;
	}
}

function deriveEndpointsFromDomain(domainConfig, overrides) {
	const publicScheme = domainConfig.public_scheme ?? 'https';
	const publicPort = domainConfig.public_port ?? (publicScheme === 'https' ? 443 : 80);
	const gatewayScheme = publicScheme === 'https' ? 'wss' : 'ws';
	const derived = {
		api: buildUrl(publicScheme, deriveDomain('api', domainConfig), publicPort, '/api'),
		app: buildUrl(publicScheme, deriveDomain('app', domainConfig), publicPort),
		gateway: buildUrl(gatewayScheme, deriveDomain('gateway', domainConfig), publicPort, '/gateway'),
		media: buildUrl(publicScheme, deriveDomain('media', domainConfig), publicPort, '/media'),
		staticCdn: buildUrl(publicScheme, deriveDomain('staticCdn', domainConfig), publicPort, '/static'),
		admin: buildUrl(publicScheme, deriveDomain('admin', domainConfig), publicPort, '/admin'),
		marketing: buildUrl(publicScheme, deriveDomain('marketing', domainConfig), publicPort, '/marketing'),
		invite: buildUrl(publicScheme, deriveDomain('invite', domainConfig), publicPort, '/invite'),
		gift: buildUrl(publicScheme, deriveDomain('gift', domainConfig), publicPort, '/gift'),
	};
	const normalizedOverrides = {
		api: normalizeOverride(overrides?.api),
		app: normalizeOverride(overrides?.app),
		gateway: normalizeOverride(overrides?.gateway),
		media: normalizeOverride(overrides?.media),
		staticCdn: normalizeOverride(overrides?.static_cdn),
		admin: normalizeOverride(overrides?.admin),
		marketing: normalizeOverride(overrides?.marketing),
		invite: normalizeOverride(overrides?.invite),
		gift: normalizeOverride(overrides?.gift),
	};
	return {
		api: normalizedOverrides.api ?? derived.api,
		app: normalizedOverrides.app ?? derived.app,
		gateway: normalizedOverrides.gateway ?? derived.gateway,
		media: normalizedOverrides.media ?? derived.media,
		staticCdn: normalizedOverrides.staticCdn ?? derived.staticCdn,
		admin: normalizedOverrides.admin ?? derived.admin,
		marketing: normalizedOverrides.marketing ?? derived.marketing,
		invite: normalizedOverrides.invite ?? derived.invite,
		gift: normalizedOverrides.gift ?? derived.gift,
	};
}

function stripApiSuffix(url) {
	if (!url) {
		return url;
	}
	return url.endsWith('/api') ? url.slice(0, -4) : url;
}

function parseSentryDsn(dsn) {
	if (!dsn) {
		return {};
	}
	try {
		const parsed = new URL(dsn);
		const path = parsed.pathname.replace(/^\/+|\/+$/g, '');
		const segments = path ? path.split('/') : [];
		const projectId = segments.length > 0 ? segments[segments.length - 1] : undefined;
		const publicKey = parsed.username || undefined;
		return {projectId, publicKey};
	} catch {
		return {};
	}
}

function resolveAppPublic(config) {
	const appPublic = getValue(config, ['app_public'], {});
	const domain = getValue(config, ['domain'], {});
	const overrides = getValue(config, ['endpoint_overrides'], {});
	const endpoints = deriveEndpointsFromDomain(domain, overrides);
	const defaultBootstrapEndpoint = endpoints.api;
	const defaultPublicEndpoint = stripApiSuffix(endpoints.api);
	const sentryDsn =
		asString(appPublic.sentry_dsn) ?? asString(getValue(config, ['services', 'app_proxy', 'sentry_dsn']));
	const sentryParsed = parseSentryDsn(sentryDsn);
	return {
		apiVersion: asString(appPublic.api_version, '1'),
		bootstrapApiEndpoint: asString(appPublic.bootstrap_api_endpoint, defaultBootstrapEndpoint),
		bootstrapApiPublicEndpoint: asString(appPublic.bootstrap_api_public_endpoint, defaultPublicEndpoint),
		relayDirectoryUrl: asString(appPublic.relay_directory_url),
		sentryDsn,
		sentryProxyPath: asString(
			appPublic.sentry_proxy_path,
			asString(getValue(config, ['services', 'app_proxy', 'sentry_proxy_path']), '/error-reporting-proxy'),
		),
		sentryReportHost: asString(
			appPublic.sentry_report_host,
			asString(getValue(config, ['services', 'app_proxy', 'sentry_report_host']), ''),
		),
		sentryProjectId: asString(appPublic.sentry_project_id, sentryParsed.projectId),
		sentryPublicKey: asString(appPublic.sentry_public_key, sentryParsed.publicKey),
	};
}

function resolveReleaseChannel() {
	const raw = asString(process.env.RELEASE_CHANNEL, 'nightly').toLowerCase();
	if (raw === 'stable' || raw === 'canary') {
		return raw;
	}
	return 'nightly';
}

function resolveBuildMetadata() {
	const envBuildSha = asString(process.env.BUILD_SHA);
	let buildSha = envBuildSha ?? undefined;
	if (!buildSha) {
		try {
			buildSha = execSync('git rev-parse --short HEAD', {cwd: ROOT_DIR, stdio: ['ignore', 'pipe', 'ignore']})
				.toString()
				.trim();
		} catch {
			buildSha = 'dev';
		}
	}
	const buildNumber = asString(process.env.BUILD_NUMBER, '0');
	const buildTimestamp = asString(process.env.BUILD_TIMESTAMP, String(Math.floor(Date.now() / 1000)));
	const releaseChannel = resolveReleaseChannel();
	return {
		buildSha,
		buildNumber,
		buildTimestamp,
		releaseChannel,
	};
}

function getPublicEnvVar(values, name) {
	const value = values[name];
	return value === undefined ? 'undefined' : JSON.stringify(value);
}

export default () => {
	const linguiSwcPlugin = getLinguiSwcPluginConfig();
	const config = readConfig();
	const appPublic = resolveAppPublic(config);
	const buildMetadata = resolveBuildMetadata();
	const publicValues = {
		PUBLIC_BUILD_SHA: buildMetadata.buildSha,
		PUBLIC_BUILD_NUMBER: buildMetadata.buildNumber,
		PUBLIC_BUILD_TIMESTAMP: buildMetadata.buildTimestamp,
		PUBLIC_RELEASE_CHANNEL: buildMetadata.releaseChannel,
		PUBLIC_SENTRY_DSN: appPublic.sentryDsn ?? null,
		PUBLIC_SENTRY_PROJECT_ID: appPublic.sentryProjectId ?? null,
		PUBLIC_SENTRY_PUBLIC_KEY: appPublic.sentryPublicKey ?? null,
		PUBLIC_SENTRY_PROXY_PATH: appPublic.sentryProxyPath,
		PUBLIC_API_VERSION: appPublic.apiVersion,
		PUBLIC_BOOTSTRAP_API_ENDPOINT: appPublic.bootstrapApiEndpoint,
		PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: appPublic.bootstrapApiPublicEndpoint ?? appPublic.bootstrapApiEndpoint,
		PUBLIC_RELAY_DIRECTORY_URL: appPublic.relayDirectoryUrl ?? null,
	};

	return {
		mode,

		entry: {
			main: path.join(SRC_DIR, 'index.tsx'),
			sw: path.join(SRC_DIR, 'service_worker', 'Worker.tsx'),
		},

		output: {
			path: DIST_DIR,
			publicPath: isProduction ? `${CDN_ENDPOINT}/` : '/',
			workerPublicPath: '/',
			filename: (pathData) => {
				if (pathData.chunk?.name === 'sw') {
					return 'sw.js';
				}
				return isProduction ? 'assets/[contenthash:16].js' : devJsName;
			},
			chunkFilename: isProduction ? 'assets/[contenthash:16].js' : devJsName,
			cssFilename: isProduction ? 'assets/[contenthash:16].css' : devCssName,
			cssChunkFilename: isProduction ? 'assets/[contenthash:16].css' : devCssName,
			assetModuleFilename: isProduction ? 'assets/[contenthash:16][ext]' : 'assets/[name].[hash][ext]',
			webAssemblyModuleFilename: isProduction ? 'assets/[contenthash:16].wasm' : 'assets/[name].[hash].wasm',
			clean: true,
		},

		devtool: 'source-map',

		target: ['web', 'browserslist'],

		resolve: {
			alias: {
				'~': SRC_DIR,
				'@app': SRC_DIR,
				'@pkgs': PKGS_DIR,
				'@fluxer/constants/src': path.join(MONOREPO_ROOT, 'packages/constants/src'),
				'@fluxer/date_utils/src': path.join(MONOREPO_ROOT, 'packages/date_utils/src'),
				'@fluxer/geo_utils/src': path.join(MONOREPO_ROOT, 'packages/geo_utils/src'),
				'@fluxer/limits/src': path.join(MONOREPO_ROOT, 'packages/limits/src'),
				'@fluxer/list_utils/src': path.join(MONOREPO_ROOT, 'packages/list_utils/src'),
				'@fluxer/markdown_parser/src': path.join(MONOREPO_ROOT, 'packages/markdown_parser/src'),
				'@fluxer/number_utils/src': path.join(MONOREPO_ROOT, 'packages/number_utils/src'),
				'@fluxer/schema/src': path.join(MONOREPO_ROOT, 'packages/schema/src'),
				'@fluxer/snowflake/src': path.join(MONOREPO_ROOT, 'packages/snowflake/src'),
				'@fluxer/ui/src': path.join(MONOREPO_ROOT, 'packages/ui/src'),
			},
			extensions: [
				'.web.tsx',
				'.web.ts',
				'.web.jsx',
				'.web.js',
				'.tsx',
				'.ts',
				'.jsx',
				'.js',
				'.json',
				'.mjs',
				'.cjs',
				'.po',
			],
		},

		module: {
			rules: [
				{
					test: /\.(tsx|ts|jsx|js)$/,
					exclude: /node_modules/,
					type: 'javascript/auto',
					parser: {
						dynamicImport: false,
					},
					use: {
						loader: 'builtin:swc-loader',
						options: {
							jsc: {
								parser: {
									syntax: 'typescript',
									tsx: true,
									decorators: true,
								},
								transform: {
									legacyDecorator: true,
									decoratorMetadata: true,
									react: {
										runtime: 'automatic',
										development: isDevelopment,
										refresh: false,
									},
								},
								experimental: {
									plugins: [linguiSwcPlugin],
								},
								target: 'es2015',
							},
						},
					},
				},

				createPoFileRule(),

				{
					test: /\.module\.css$/,
					use: [{loader: 'postcss-loader'}],
					type: 'css/module',
					parser: {namedExports: false},
				},
				{
					test: /\.css$/,
					exclude: /\.module\.css$/,
					use: [{loader: 'postcss-loader'}],
					type: 'css',
				},

				{
					test: /\.svg$/,
					issuer: /\.[jt]sx?$/,
					resourceQuery: /react/,
					type: 'javascript/auto',
					use: [
						{
							loader: 'builtin:swc-loader',
							options: {
								jsc: {
									parser: {syntax: 'typescript', tsx: true},
									transform: {react: {runtime: 'automatic', development: isDevelopment}},
									target: 'es2015',
								},
							},
						},
						{
							loader: '@svgr/webpack',
							options: {
								babel: false,
								typescript: true,
								jsxRuntime: 'automatic',
								svgoConfig: {
									plugins: [
										{
											name: 'preset-default',
											params: {overrides: {removeViewBox: false}},
										},
									],
								},
							},
						},
					],
				},
				{
					test: /\.svg$/,
					resourceQuery: {not: [/react/]},
					type: 'asset/resource',
				},

				{
					test: /\.wasm$/,
					type: 'asset/resource',
				},
				{
					test: /\.(png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|eot|mp3|wav|ogg|mp4|webm)$/,
					type: 'asset/resource',
					generator: {
						filename: isProduction ? 'assets/[contenthash:16][ext]' : 'assets/[name].[hash][ext]',
					},
				},
			],

			generator: {
				'css/module': {
					localIdentName: '[name]__[local]___[hash:base64:6]',
					exportsConvention: 'camel-case-only',
					exportsOnly: false,
				},
				'css/auto': {
					localIdentName: '[name]__[local]___[hash:base64:6]',
					exportsConvention: 'camel-case-only',
					exportsOnly: false,
				},
			},
		},

		plugins: [
			new HtmlRspackPlugin({
				template: path.join(ROOT_DIR, 'index.html'),
				filename: 'index.html',
				inject: 'body',
				scriptLoading: 'module',
				excludeChunks: ['sw'],
			}),

			new CopyRspackPlugin({
				patterns: [
					{
						from: PUBLIC_DIR,
						to: DIST_DIR,
						noErrorOnMissing: true,
					},
				],
			}),

			staticFilesPlugin({staticCdnEndpoint: CDN_ENDPOINT}),

			new DefinePlugin({
				'process.env.NODE_ENV': JSON.stringify(mode),
				'import.meta.env.DEV': JSON.stringify(isDevelopment),
				'import.meta.env.PROD': JSON.stringify(isProduction),
				'import.meta.env.MODE': JSON.stringify(mode),
				'import.meta.env.PUBLIC_BUILD_SHA': getPublicEnvVar(publicValues, 'PUBLIC_BUILD_SHA'),
				'import.meta.env.PUBLIC_BUILD_NUMBER': getPublicEnvVar(publicValues, 'PUBLIC_BUILD_NUMBER'),
				'import.meta.env.PUBLIC_BUILD_TIMESTAMP': getPublicEnvVar(publicValues, 'PUBLIC_BUILD_TIMESTAMP'),
				'import.meta.env.PUBLIC_RELEASE_CHANNEL': getPublicEnvVar(publicValues, 'PUBLIC_RELEASE_CHANNEL'),
				'import.meta.env.PUBLIC_SENTRY_DSN': getPublicEnvVar(publicValues, 'PUBLIC_SENTRY_DSN'),
				'import.meta.env.PUBLIC_SENTRY_PROJECT_ID': getPublicEnvVar(publicValues, 'PUBLIC_SENTRY_PROJECT_ID'),
				'import.meta.env.PUBLIC_SENTRY_PUBLIC_KEY': getPublicEnvVar(publicValues, 'PUBLIC_SENTRY_PUBLIC_KEY'),
				'import.meta.env.PUBLIC_SENTRY_PROXY_PATH': getPublicEnvVar(publicValues, 'PUBLIC_SENTRY_PROXY_PATH'),
				'import.meta.env.PUBLIC_API_VERSION': getPublicEnvVar(publicValues, 'PUBLIC_API_VERSION'),
				'import.meta.env.PUBLIC_BOOTSTRAP_API_ENDPOINT': getPublicEnvVar(publicValues, 'PUBLIC_BOOTSTRAP_API_ENDPOINT'),
				'import.meta.env.PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT': getPublicEnvVar(
					publicValues,
					'PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT',
				),
				'import.meta.env.PUBLIC_RELAY_DIRECTORY_URL': getPublicEnvVar(publicValues, 'PUBLIC_RELAY_DIRECTORY_URL'),
			}),
		],

		optimization: {
			splitChunks: isProduction
				? {
						chunks: (chunk) => chunk.name !== 'sw',
						maxInitialRequests: 50,
						cacheGroups: {
							icons: {
								test: /[\\/]node_modules[\\/]@phosphor-icons[\\/]/,
								name: 'icons',
								priority: 60,
								reuseExistingChunk: true,
							},
							highlight: {
								test: /[\\/]node_modules[\\/]highlight\.js[\\/]/,
								name: 'highlight',
								priority: 55,
								reuseExistingChunk: true,
							},
							livekit: {
								test: /[\\/]node_modules[\\/](livekit-client|@livekit)[\\/]/,
								name: 'livekit',
								priority: 50,
								reuseExistingChunk: true,
							},
							katex: {
								test: /[\\/]node_modules[\\/]katex[\\/]/,
								name: 'katex',
								priority: 48,
								reuseExistingChunk: true,
							},
							animation: {
								test: /[\\/]node_modules[\\/](framer-motion|motion)[\\/]/,
								name: 'animation',
								priority: 45,
								reuseExistingChunk: true,
							},
							mobx: {
								test: /[\\/]node_modules[\\/](mobx|mobx-react-lite|mobx-persist-store)[\\/]/,
								name: 'mobx',
								priority: 43,
								reuseExistingChunk: true,
							},
							sentry: {
								test: /[\\/]node_modules[\\/]@sentry[\\/]/,
								name: 'sentry',
								priority: 41,
								reuseExistingChunk: true,
							},
							reactAria: {
								test: /[\\/]node_modules[\\/]react-aria-components[\\/]/,
								name: 'react-aria',
								priority: 40,
								reuseExistingChunk: true,
							},
							validation: {
								test: /[\\/]node_modules[\\/](valibot)[\\/]/,
								name: 'validation',
								priority: 38,
								reuseExistingChunk: true,
							},
							datetime: {
								test: /[\\/]node_modules[\\/]luxon[\\/]/,
								name: 'datetime',
								priority: 37,
								reuseExistingChunk: true,
							},
							observable: {
								test: /[\\/]node_modules[\\/]rxjs[\\/]/,
								name: 'observable',
								priority: 36,
								reuseExistingChunk: true,
							},
							unicode: {
								test: /[\\/]node_modules[\\/](idna-uts46-hx|emoji-regex)[\\/]/,
								name: 'unicode',
								priority: 35,
								reuseExistingChunk: true,
							},
							dnd: {
								test: /[\\/]node_modules[\\/](@dnd-kit|react-dnd)[\\/]/,
								name: 'dnd',
								priority: 33,
								reuseExistingChunk: true,
							},
							radix: {
								test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
								name: 'radix',
								priority: 31,
								reuseExistingChunk: true,
							},
							ui: {
								test: /[\\/]node_modules[\\/](react-select|react-hook-form|react-modal-sheet|react-zoom-pan-pinch|@floating-ui)[\\/]/,
								name: 'ui',
								priority: 30,
								reuseExistingChunk: true,
							},
							utils: {
								test: /[\\/]node_modules[\\/](lodash|clsx|qrcode|thumbhash|bowser|match-sorter)[\\/]/,
								name: 'utils',
								priority: 28,
								reuseExistingChunk: true,
							},
							networking: {
								test: /[\\/]node_modules[\\/](ws)[\\/]/,
								name: 'networking',
								priority: 26,
								reuseExistingChunk: true,
							},
							react: {
								test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
								name: 'react',
								priority: 25,
								reuseExistingChunk: true,
							},
							vendor: {
								test: /[\\/]node_modules[\\/]/,
								name: 'vendor',
								priority: 10,
								reuseExistingChunk: true,
							},
						},
					}
				: false,
			runtimeChunk: false,
			chunkSplit: false,
			moduleIds: 'named',
			chunkIds: 'named',
			minimize: isProduction,
			minimizer: [
				new SwcJsMinimizerRspackPlugin({
					compress: true,
					mangle: true,
					format: {comments: false},
				}),
			],
		},

		devServer: {
			port: Number(process.env.FLUXER_APP_DEV_PORT) || 49427,
			hot: false,
			liveReload: false,
			client: false,
			webSocketServer: false,
			historyApiFallback: true,
			allowedHosts: 'all',
			headers: {
				'Access-Control-Allow-Origin': '*',
				'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
				'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
			},
			static: {
				directory: DIST_DIR,
				watch: false,
			},
		},

		experiments: {css: true},
	};
};
