import { Resource } from '../resources/Resource.ts';
import { contextStorage, transaction } from '../resources/transaction.ts';
import { RequestTarget } from '../resources/RequestTarget.ts';
import { tables, databases } from '../resources/databases.ts';
import { models as harperModelsSingleton } from '../resources/models/Models.ts';
import { readFile } from 'node:fs/promises';
import { dirname, isAbsolute } from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
import { SourceTextModule, SyntheticModule, createContext, runInContext, runInThisContext } from 'node:vm';
import { ApplicationScope } from '../components/ApplicationScope.ts';
import logger from '../utility/logging/harper_logger.ts';
import { createRequire } from 'node:module';
import * as env from '../utility/environment/environmentManager';
import * as child_process from 'node:child_process';
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
import { contentTypes } from '../server/serverHelpers/contentTypes.ts';
import type {} from 'ses';
import {
	mkdirSync,
	readFileSync,
	writeFileSync,
	unlinkSync,
	openSync,
	closeSync,
	statSync,
	realpathSync,
} from 'node:fs';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';
import { whenComponentsLoaded } from '../server/threads/threadServer.js';

type Lockdown = 'none' | 'freeze' | 'ses' | 'freeze-after-load';
const APPLICATIONS_LOCKDOWN: Lockdown = env.get(CONFIG_PARAMS.APPLICATIONS_LOCKDOWN);
const HARPER_MODULE_IDS = new Set([
	'harper',
	'harperdb',
	'harperdb/v1',
	'harperdb/v2',
	'@harperfast/harper',
	'@harperfast/harper-pro',
]);

// `harper.models` (consumed by `getHarperExports`) and the top-level
// `models` package export both point at the same process-wide `Models`
// singleton declared in `resources/models/Models.ts`. The Models class has
// no per-Scope or per-ApplicationScope state (registry + analytics writer
// are process-singletons), so one shared instance is observationally
// identical to the per-Scope instances built in `components/Scope.ts`.

let lockedDown = false;
/**
 * This is the main entry point for loading plugin and application modules that may be executed in a
 * separate top level scope. The scope indicates if we use a different top level scope or a standard import.
 * @param moduleUrl
 * @param scope
 */
export async function scopedImport(filePath: string | URL, scope?: ApplicationScope) {
	if (!lockedDown && APPLICATIONS_LOCKDOWN && APPLICATIONS_LOCKDOWN !== 'none') {
		lockedDown = true;
		if (APPLICATIONS_LOCKDOWN === 'ses') {
			require('ses'); // load the lockdown function
			lockdown({
				domainTaming: 'unsafe',
				consoleTaming: 'unsafe',
				errorTaming: 'unsafe',
				errorTrapping: 'none',
				stackFiltering: 'verbose',
			});
		} else {
			preventFunctionConstructor();
			if (APPLICATIONS_LOCKDOWN === 'freeze-after-load') {
				whenComponentsLoaded.then(freezeIntrinsics);
			} else {
				freezeIntrinsics();
			}
		}
	}
	const moduleUrl = (filePath instanceof URL ? filePath : pathToFileURL(filePath)).toString();
	try {
		const loaderMode = scope?.mode;
		if (scope && loaderMode !== 'native') {
			if (loaderMode === 'compartment') {
				// use SES Compartments
				// note that we use a single compartment per scope and we load it on-demand, only
				// loading if necessary (since it is actually very heavy)
				const globals = getGlobalObject(scope);
				if (!scope.compartment) scope.compartment = getCompartment(scope, globals);
				const result = await (await scope.compartment).import(moduleUrl);
				return result.namespace;
			} else if (loaderMode === 'vm-current-context' && SourceTextModule) {
				// Use VM module loader with current context (shares intrinsics, no custom globals)
				return await loadModuleWithVM(moduleUrl, scope, false);
			} else if (SourceTextModule) {
				// Use VM module to do module loading with custom context (sandboxed)
				return await loadModuleWithVM(moduleUrl, scope, true);
			}
		}
		// important! we need to await the import, otherwise the error will not be caught
		return await import(moduleUrl);
	} catch (err) {
		try {
			// the actual parse error (internally known as the "arrow message")
			// is hidden behind a private symbol (arrowMessagePrivateSymbol)
			// on the error object and the only way to access it is to use the
			// internal util.decorateErrorStack() function
			// @ts-ignore
			const util = await import('internal/util');
			util.default.decorateErrorStack(err);
		} catch {
			// maybe --expose-internals was not set?
		}
		throw err;
	}
}

let amaro: typeof import('amaro') | undefined;
/**
 * Strip TypeScript types synchronously using the amaro library (what Node.js uses internally)
 */
function stripTypeScriptTypes(source: string): string {
	// Use amaro - the library that Node.js uses internally for type stripping
	if (!amaro) {
		amaro = require('amaro');
	}
	return amaro.transformSync(source, { mode: 'strip-only' }).code;
}

/**
 * Parse a JSON string and return the resulting object. Wraps JSON.parse errors
 * with the module URL for easier debugging.
 */
function parseJsonModule(source: string, url: string): any {
	try {
		return JSON.parse(source);
	} catch (err) {
		throw new Error(`Failed to parse JSON module ${url}: ${err.message}`);
	}
}

/**
 * Walk an exports-map entry (string, array, or conditions object) with the given
 * condition priority list and return the first matching path string, or null.
 */
function walkExportsConditions(entry: unknown, conditions: readonly string[]): string | null {
	if (typeof entry === 'string') return entry;
	if (Array.isArray(entry)) {
		for (const e of entry) {
			const r = walkExportsConditions(e, conditions);
			if (r !== null) return r;
		}
		return null;
	}
	if (entry !== null && typeof entry === 'object') {
		for (const cond of conditions) {
			const val = (entry as Record<string, unknown>)[cond];
			if (val !== undefined) {
				const r = walkExportsConditions(val, conditions);
				if (r !== null) return r;
			}
		}
	}
	return null;
}

/**
 * Resolve a bare package specifier to a file:// URL using the package's exports map
 * with ESM import conditions. Used as a fallback when createRequire().resolve() throws
 * ERR_PACKAGE_PATH_NOT_EXPORTED for pure-ESM packages (exports map with only "import"
 * conditions and no "require").
 */
function resolveESMPackageExports(specifier: string, fromDir: string): string | null {
	const isScoped = specifier.startsWith('@');
	const parts = specifier.split('/');
	const packageName = isScoped ? `${parts[0]}/${parts[1]}` : parts[0];
	const subpathParts = parts.slice(isScoped ? 2 : 1);
	const subpath = subpathParts.length > 0 ? './' + subpathParts.join('/') : '.';

	let dir = fromDir;
	while (true) {
		const pkgRoot = join(dir, 'node_modules', packageName);
		let pkgJson: any;
		try {
			pkgJson = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8'));
		} catch {
			const parent = dirname(dir);
			if (parent === dir) return null;
			dir = parent;
			continue;
		}

		if (!pkgJson.exports) return null;

		const exportsMap = pkgJson.exports;
		let entry: unknown;
		if (typeof exportsMap === 'string') {
			entry = subpath === '.' ? exportsMap : undefined;
		} else if (exportsMap !== null && typeof exportsMap === 'object') {
			const firstKey = Object.keys(exportsMap)[0];
			if (firstKey?.startsWith('.')) {
				// subpath map: { ".": ..., "./foo": ... }
				entry = (exportsMap as Record<string, unknown>)[subpath];
			} else {
				// conditions map at root: { "import": ..., "require": ... }
				entry = subpath === '.' ? exportsMap : undefined;
			}
		}

		if (!entry) return null;

		const relative = walkExportsConditions(entry, ['import', 'node', 'default']);
		if (!relative) return null;

		return pathToFileURL(join(pkgRoot, relative)).toString();
	}
}

/**
 * Load a module using Node's vm.Module API with optional sandboxing
 * @param moduleUrl - The URL of the module to load
 * @param scope - The application scope
 * @param useCustomContext - If true, uses a sandboxed context with custom globals. If false, uses current context.
 */
async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope, useCustomContext: boolean = true) {
	// we want to retain the same module caches across any loading with the application scope
	let moduleCaches = scope.moduleCache as {
		moduleCache: Map<string, SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule>>;
		linkingPromises: Map<string, Promise<void>>;
		cjsCache: Map<string, { exports: any }>;
		contextObject: any;
		context: any;
	};
	if (!moduleCaches) {
		// if they haven't been initialized, do so now
		let contextObject: any, context: any;
		if (useCustomContext) {
			// Create a sandboxed context with custom globals
			contextObject = getGlobalObject(scope, true);
			context = createContext(contextObject);
		} else {
			// Use current context (no sandboxing, shares intrinsics with parent)
			contextObject = undefined;
			context = undefined;
		}

		moduleCaches = scope.moduleCache = {
			moduleCache: new Map(),
			linkingPromises: new Map(),
			cjsCache: new Map(),
			contextObject,
			context,
		};
	}
	const { moduleCache, linkingPromises, cjsCache, contextObject, context } = moduleCaches;

	/**
	 * Resolve module specifier to absolute URL
	 */
	function resolveModule(specifier: string, referrer: string): string {
		if (HARPER_MODULE_IDS.has(specifier)) {
			return 'harper'; // resolve any harper package as an alias to a single synthetic module
		}
		const parts = specifier.split('/');
		if (parts[0] === 'harper') {
			// block harper/* for now (reserving for potential future use)
			throw new Error(`Module ${specifier} is not allowed, may only access the 'harper' module`);
		}
		if (parts[0] === 'file:') {
			return specifier;
		}
		let resolveReferrer = referrer;
		if (referrer.startsWith('file:')) {
			try {
				resolveReferrer = pathToFileURL(realpathSync(fileURLToPath(referrer))).toString();
			} catch {}
		}
		try {
			const resolved = createRequire(resolveReferrer).resolve(specifier);
			if (isAbsolute(resolved)) {
				return pathToFileURL(resolved).toString();
			}
			return resolved;
		} catch (err) {
			if ((err as any)?.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
				// Pure-ESM package: CJS resolver cannot match an exports map that only has
				// "import" conditions (no "require"). Resolve the entry file by walking
				// the filesystem and evaluating the exports map with ESM import conditions.
				const referrerDir = resolveReferrer.startsWith('file:')
					? dirname(fileURLToPath(resolveReferrer))
					: dirname(resolveReferrer);
				const esmResolved = resolveESMPackageExports(specifier, referrerDir);
				if (esmResolved) return esmResolved;
			}
			throw err;
		}
	}

	/**
	 * Load a CommonJS module in our context (private or current)
	 */
	function loadCJS(url: string, source: string): { exports: any } {
		// Check cache first to handle circular dependencies
		if (cjsCache.has(url)) {
			return cjsCache.get(url)!;
		}

		// Create module object and cache it immediately (before execution)
		// This allows circular dependencies to get a reference to the incomplete module
		const cjsModule = { exports: {} };
		cjsCache.set(url, cjsModule);

		if (url.endsWith('.json')) {
			cjsModule.exports = parseJsonModule(source, url);
			return cjsModule;
		}
		let requireUrl = url;
		if (url.startsWith('file://')) {
			try {
				requireUrl = pathToFileURL(realpathSync(fileURLToPath(url))).toString();
			} catch {}
		}
		const require = createRequire(requireUrl);

		const cjsRequire = (spec: string) => {
			const resolvedUrl = resolveModule(spec, url);
			if (resolvedUrl === 'harper') {
				return getHarperExports(scope);
			}
			if (resolvedUrl.startsWith('file://')) {
				const source = readFileSync(new URL(resolvedUrl), { encoding: 'utf-8' });
				return loadCJS(resolvedUrl, source).exports;
			}
			return require(resolvedUrl);
		};
		cjsRequire.resolve = (spec: string) => {
			const resolvedUrl = resolveModule(spec, url);
			if (resolvedUrl.startsWith('file://')) return fileURLToPath(resolvedUrl);
			return resolvedUrl;
		};

		const cjsWrapper = `
			(function(module, exports, require, __filename, __dirname) {
				${source}
			})
		`;

		const runOptions = {
			filename: url,
			async importModuleDynamically(specifier: string, script) {
				const resolvedUrl = resolveModule(specifier, script?.sourceURL ?? url);
				const useApplicationLoader = shouldUseApplicationLoader(specifier, resolvedUrl);
				const dynamicModule = await loadModuleWithCache(resolvedUrl, useApplicationLoader);
				return dynamicModule;
			},
		};

		// If contextObject is undefined, use current context with runInThisContext
		const wrappedFn = contextObject
			? runInContext(cjsWrapper, contextObject, runOptions)
			: runInThisContext(cjsWrapper, runOptions);

		wrappedFn(
			cjsModule,
			cjsModule.exports,
			cjsRequire,
			url,
			dirname(url.startsWith('file://') ? fileURLToPath(url) : url)
		);

		return cjsModule;
	}
	function loadCJSModule(url: string, source: string, usePrivateGlobal: boolean): SyntheticModule {
		const cjsModule = usePrivateGlobal ? loadCJS(url, source) : { exports: require(url) };
		let exports = cjsModule.exports;
		if (exports.default === undefined) {
			// provide the default export for compatibility
			exports = { default: exports, ...exports };
		}
		const exportNames = Object.keys(exports);

		const options: any = { identifier: url };
		if (usePrivateGlobal && context) {
			options.context = context;
		}

		const synModule = new SyntheticModule(
			exportNames,
			function () {
				for (const key of exportNames) {
					this.setExport(key, exports[key]);
				}
			},
			options
		);
		// Don't cache here - let getOrCreateModule handle caching
		return synModule;
	}

	/**
	 * Check if a package (or any of its dependencies) depends on harper
	 * Expects a file URL like: file:///path/to/node_modules/package-name/dist/index.js
	 */
	function packageDependsOnHarper(fileUrl: string): boolean {
		try {
			// Convert file:// URL to path
			const filePath = fileURLToPath(fileUrl);

			// Find the node_modules directory and package name
			// Example: /path/to/node_modules/package-name/dist/index.js
			// or: /path/to/node_modules/@scope/package-name/dist/index.js
			const nodeModulesMarker = '/node_modules/';
			const nodeModulesIndex = filePath.lastIndexOf(nodeModulesMarker);
			if (nodeModulesIndex === -1) return false;

			// Get the part after /node_modules/
			const afterNodeModules = filePath.substring(nodeModulesIndex + nodeModulesMarker.length);
			const parts = afterNodeModules.split('/');

			// Handle scoped packages (@scope/package-name) vs regular packages (package-name)
			const beforeNodeModules = filePath.substring(0, nodeModulesIndex);
			const packageRoot = parts[0].startsWith('@')
				? join(beforeNodeModules, 'node_modules', parts[0], parts[1])
				: join(beforeNodeModules, 'node_modules', parts[0]);

			// Read package.json from the package root
			const packageJsonPath = join(packageRoot, 'package.json');
			const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));

			const deps = {
				...packageJson.dependencies,
				...packageJson.devDependencies,
				...packageJson.peerDependencies,
			};

			// Check if harper is a direct dependency
			return Object.keys(deps).some((dep) => HARPER_MODULE_IDS.has(dep));
		} catch {
			return false;
		}
	}

	/**
	 * Determine if a module should use application VM module loader based on specifier and resolved URL
	 */
	function shouldUseApplicationLoader(specifier: string, resolvedUrl: string): boolean {
		// Always contain relative imports
		if (specifier.startsWith('.')) {
			return true;
		}

		// Check the dependencyLoader for definitive settings, if it is native we always use native loader
		if (scope.dependencyLoader === 'native') {
			return false;
		}

		// If it is set to always use the app module loader
		if (scope.dependencyLoader === 'app') {
			return true;
		}

		// For npm packages, check if they depend on harper
		if (resolvedUrl.startsWith('file://') && resolvedUrl.includes('/node_modules/')) {
			return packageDependsOnHarper(resolvedUrl);
		}
		return false;
	}

	/**
	 * Linker function for module resolution during instantiation.
	 * This is synchronous because Node's module.link() requires the linker
	 * to return modules synchronously.
	 */
	function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
		const resolvedUrl = resolveModule(specifier, referencingModule.identifier);
		const useApplicationLoader = shouldUseApplicationLoader(specifier, resolvedUrl);
		return getOrCreateModule(resolvedUrl, useApplicationLoader);
	}

	function getOrCreateModule(
		url: string,
		usePrivateGlobal: boolean
	): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
		// Check if module is already created
		if (moduleCache.has(url)) {
			return moduleCache.get(url)!;
		}

		// Create the module and cache it immediately (before linking)
		const module = createModule(url, usePrivateGlobal);
		moduleCache.set(url, module);

		return module;
	}

	async function loadModuleWithCache(
		url: string,
		usePrivateGlobal: boolean
	): Promise<SourceTextModule | SyntheticModule> {
		const module = await getOrCreateModule(url, usePrivateGlobal);

		// Only link/evaluate once per module
		if (!linkingPromises.has(url)) {
			const linkingPromise = (async () => {
				// Check module status - only link if it's 'unlinked'
				// Status can be: 'unlinked', 'linking', 'linked', 'evaluating', 'evaluated'
				if (module.status === 'unlinked') {
					await module.link(linker);
				}
				// Only evaluate if not already evaluated
				if (module.status === 'linked') {
					await module.evaluate();
				}
			})();
			linkingPromises.set(url, linkingPromise);
		}

		// Wait for linking to complete
		await linkingPromises.get(url);

		return module;
	}
	/**
	 * Create a SyntheticModule from exported object
	 */
	function createSyntheticModule(url: string, exportedObject: any): SyntheticModule {
		const exportNames = Object.keys(exportedObject);
		return new SyntheticModule(
			exportNames,
			function () {
				for (const key of exportNames) {
					this.setExport(key, exportedObject[key]);
				}
			},
			{ identifier: url, context }
		);
	}

	/**
	 * Normalize imported module to ensure it has proper exports including default
	 */
	function normalizeImportedModule(importedModule: any): any {
		const cjsModule = importedModule['module.exports'];
		if (cjsModule) {
			// back-compat import
			importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
		}
		// Ensure there's a default export for ESM imports that expect it
		if (!importedModule.default) {
			importedModule = { default: importedModule, ...importedModule };
		}
		return importedModule;
	}

	/**
	 * Create a SourceTextModule or SyntheticModule from source code
	 */
	function createModuleFromSource(
		url: string,
		source: string,
		usePrivateGlobal: boolean
	): SourceTextModule | SyntheticModule {
		// Handle JSON modules
		if (url.endsWith('.json')) {
			const jsonData = parseJsonModule(source, url);
			return new SyntheticModule(
				['default'],
				function () {
					this.setExport('default', jsonData);
				},
				{ identifier: url, context }
			);
		}

		// Strip TypeScript types if this is a .ts file
		if (url.endsWith('.ts') || url.endsWith('.tsx')) {
			source = stripTypeScriptTypes(source);
		}

		// Try CJS first since it will fail fast with clear syntax errors on ESM syntax
		try {
			return loadCJSModule(url, source, usePrivateGlobal);
		} catch (cjsErr) {
			// Only fallback to ESM if the error was due to ESM syntax (import/export statements)
			// Check if the error message indicates ESM syntax
			const errorMessage = cjsErr?.message || '';
			const isEsmSyntaxError =
				errorMessage.includes('import') ||
				errorMessage.includes('export') ||
				errorMessage.includes('Cannot use import statement') ||
				errorMessage.includes('Unexpected token');

			if (!isEsmSyntaxError) {
				// This was a real error (not ESM syntax), rethrow it
				throw cjsErr;
			}

			// If CJS loading fails due to ESM syntax, try ESM
			return new SourceTextModule(source, {
				identifier: url,
				context,
				initializeImportMeta(meta) {
					meta.url = url;
					if (url.startsWith('file:')) {
						meta.filename = fileURLToPath(url);
						meta.dirname = dirname(meta.filename);
					}
					meta.resolve = (specifier: string) => resolveModule(specifier, url);
				},
				importModuleDynamically(specifier: string) {
					const resolvedUrl = resolveModule(specifier, url);
					const useApplicationLoader = shouldUseApplicationLoader(specifier, resolvedUrl);
					return loadModuleWithCache(resolvedUrl, useApplicationLoader);
				},
			});
		}
	}

	/**
	 * Create a module from URL without linking or evaluating (async version for initial load)
	 */
	function createModule(
		url: string,
		usePrivateGlobal: boolean
	): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
		// Handle special built-in modules
		if (url === 'harper') {
			return createSyntheticModule(url, getHarperExports(scope));
		}

		if (url.startsWith('file://') && usePrivateGlobal) {
			checkAllowedModulePath(url, scope.allowedPath);
			const source = readFileSync(new URL(url), { encoding: 'utf-8' });
			return createModuleFromSource(url, source, usePrivateGlobal);
		}

		// For Node.js built-in modules (node:) and npm packages without application loader for dependency
		const replacedModule = checkAllowedModulePath(url, scope.allowedPath);
		if (replacedModule) {
			return createSyntheticModule(url, normalizeImportedModule(replacedModule));
		}
		return import(url).then((importedModule) => createSyntheticModule(url, normalizeImportedModule(importedModule)));
	}
	// Load the entry module
	const entryModule = await loadModuleWithCache(moduleUrl, true);

	// Return the module namespace (exports)
	return entryModule.namespace;
}

async function getCompartment(scope: ApplicationScope, globals) {
	const { StaticModuleRecord } = await import('@endo/static-module-record');
	require('ses');
	const compartment: any = new (Compartment as any)(
		globals,
		{
			//harperdb: { Resource, tables, databases }
		},
		{
			name: 'harper-app',
			resolveHook(moduleSpecifier, moduleReferrer) {
				if (HARPER_MODULE_IDS.has(moduleSpecifier)) {
					return 'harper'; // resolve any harper package as an alias to a single synthetic module
				}
				const parts = moduleSpecifier.split('/');
				if (parts[0] === 'harper') {
					// block harper/* for now (reserving for potential future use)
					throw new Error(`Module ${moduleSpecifier} is not allowed, may only access the 'harper' module`);
				}

				const resolved = createRequire(moduleReferrer).resolve(moduleSpecifier);
				if (isAbsolute(resolved)) {
					const resolvedURL = pathToFileURL(resolved).toString();
					return resolvedURL;
				}
				return moduleSpecifier;
			},
			importHook: async (moduleSpecifier) => {
				if (moduleSpecifier === 'harper') {
					const harperExports = getHarperExports(scope);
					return {
						imports: [],
						exports: Object.keys(harperExports),
						execute(exports) {
							Object.assign(exports, harperExports);
						},
					};
				} else if (moduleSpecifier.startsWith('file:') && !moduleSpecifier.includes('node_modules')) {
					let moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
					// Handle JSON files in compartment mode the same way as in VM mode
					if (moduleSpecifier.endsWith('.json')) {
						const jsonData = parseJsonModule(moduleText, moduleSpecifier);
						return {
							imports: [],
							exports: ['default'],
							execute(exports) {
								exports.default = jsonData;
							},
						};
					}
					// Strip TypeScript types if this is a .ts file
					if (moduleSpecifier.endsWith('.ts') || moduleSpecifier.endsWith('.tsx')) {
						moduleText = stripTypeScriptTypes(moduleText);
					}
					return new StaticModuleRecord(moduleText, moduleSpecifier);
				} else {
					checkAllowedModulePath(moduleSpecifier, scope.allowedPath);
					const moduleExports = await import(moduleSpecifier);
					return {
						imports: [],
						exports: Object.keys(moduleExports),
						execute(exports) {
							for (const key of Object.keys(moduleExports)) {
								exports[key] = moduleExports[key];
							}
						},
					};
				}
			},
		}
	);
	return compartment;
}

/**
 * This a constrained fetch. It certainly is not guaranteed to be safe, but requiring https may
 * be a good heuristic for preventing access to unsecured resources within a private network.
 * @param resource
 * @param options
 */
function secureOnlyFetch(resource, options) {
	// TODO: or maybe we should constrain by doing a DNS lookup and having disallow list of IP addresses that includes
	// this server
	const url = typeof resource === 'string' || resource.url;
	if (new URL(url).protocol != 'https') throw new Error('Only https is allowed in fetch');
	return fetch(resource, options);
}

// These globals need to match the literals produced in the VM context
const contextualizedJSGlobals = ['Object', 'Array', 'Function', 'globalThis'];

let defaultJSGlobalNames: string[];
// get the global variable names that are intrinsically present in a VM context (so we don't override them)
function getDefaultJSGlobalNames() {
	if (!defaultJSGlobalNames) {
		defaultJSGlobalNames = runInContext(
			'Object.getOwnPropertyNames((function() { return this })())',
			createContext({})
		);
	}
	return defaultJSGlobalNames;
}

/**
 * Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
 *
 * The compartment global is SEEDED from the process `global` — every Harper process-global
 * (`tables`, `databases`, `Resource`, …; see ../globals.js) is copied across as a live reference,
 * not a per-compartment copy. A compartment is therefore not given an alternate set of these
 * objects; it shares the same ones as the rest of the process (with only `server`/`logger`/
 * `resources`/`config` optionally overridden per scope).
 */
function getGlobalObject(scope: ApplicationScope, copyIntrinsics = false) {
	const appGlobal = {};
	// create the new global object, assigning all the global variables from this global
	// except those that will be natural intrinsics of the new VM
	const globalsToExclude = copyIntrinsics ? contextualizedJSGlobals : getDefaultJSGlobalNames();
	for (let name of Object.getOwnPropertyNames(global)) {
		if (globalsToExclude.includes(name)) continue;
		appGlobal[name] = global[name];
	}
	// now assign Harper scope-specific variables
	Object.assign(appGlobal, {
		server: scope.server ?? server,
		logger: scope.logger ?? logger,
		resources: scope.resources,
		config: scope.config ?? {},
		fetch: APPLICATIONS_LOCKDOWN === 'ses' ? secureOnlyFetch : fetch,
		console,
		global: appGlobal,
		harper: getHarperExports(scope),
	});
	return appGlobal;
}
// The object exposed as the `harper` module inside compartments (`import { tables } from 'harper'`).
// `Resource`/`tables`/`databases`/`createBlob`/… below are the live, process-wide singletons (the
// same values surfaced as the top-level package exports and bare globals), not per-compartment
// copies — only `server`/`logger`/`resources`/`config` are scope-overridable.
function getHarperExports(scope: ApplicationScope) {
	return {
		server: scope.server ?? server,
		logger: scope.logger ?? logger,
		resources: scope.resources,
		config: scope.config ?? {},
		Resource,
		tables,
		databases,
		// `harper.models` — same singleton that's surfaced as the top-level
		// `models` package export (see `resources/models/Models.ts`).  The
		// registry it reads from is populated at boot by
		// `resources/models/bootstrap.ts`. The backend-registration API
		// (`registerBackend` / `defineBackend`) and custom routing
		// (`registerRouter`) are methods on this singleton (#1534, #1326) —
		// components reach them via `models.registerBackend(...)`, not as
		// separate top-level exports.
		models: harperModelsSingleton,
		createBlob,
		RequestTarget,
		getContext,
		transaction,
		getResponse,
		getUser,
		authenticateUser: server.authenticateUser,
		operation: server.operation,
		contentTypes,
		Attribute: undefined,
		Config: undefined,
		ConfigValue: undefined,
		Context: undefined,
		FileAndURLPathConfig: undefined,
		FilesOption: undefined,
		FilesOptionObject: undefined,
		IterableEventQueue: undefined,
		Logger: undefined,
		Query: undefined,
		RecordObject: undefined,
		RequestTargetOrId: undefined,
		ResourceInterface: undefined,
		Scope: undefined,
		Session: undefined,
		SourceContext: undefined,
		SubscriptionRequest: undefined,
		Table: undefined,
		User: undefined,
	};
}
const ALLOWED_NODE_BUILTIN_MODULES = env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDBUILTINMODULES)
	? new Set(env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDBUILTINMODULES))
	: {
			// if we don't have a list of allowed modules, allow everything
			has() {
				return true;
			},
		};
const ALLOWED_COMMANDS = new Set(env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDSPAWNCOMMANDS) ?? []);
const child_processConstrained: any = {
	exec: createSpawn(child_process.exec),
	execFile: createSpawn(child_process.execFile),
	fork: createSpawn(child_process.fork, true), // this is launching node, so deemed safe
	spawn: createSpawn(child_process.spawn),
	execSync: function () {
		throw new Error('execSync is not allowed');
	},
};
child_processConstrained.default = child_processConstrained;
const REPLACED_BUILTIN_MODULES = {
	child_process: child_processConstrained,
};
/**
 * Creates a ChildProcess-like object for an existing process
 */
class ExistingProcessWrapper extends EventEmitter {
	pid: number;
	private checkInterval: NodeJS.Timeout;

	constructor(pid: number) {
		super();
		this.pid = pid;

		// Monitor process and emit exit event when it terminates
		this.checkInterval = setInterval(() => {
			try {
				// Signal 0 checks if process exists without actually killing it
				process.kill(pid, 0);
			} catch {
				// Process no longer exists
				clearInterval(this.checkInterval);
				this.emit('exit', null, null);
			}
		}, 1000);
	}

	// Kill the process
	kill(signal?: NodeJS.Signals | number) {
		try {
			process.kill(this.pid, signal);
			return true;
		} catch {
			return false;
		}
	}

	// Clean up interval when wrapper is no longer needed
	unref() {
		clearInterval(this.checkInterval);
		return this;
	}
}

/**
 * Checks if a process with the given PID is running
 */
function isProcessRunning(pid: number): boolean {
	try {
		// Signal 0 checks existence without killing
		process.kill(pid, 0);
		return true;
	} catch {
		return false;
	}
}

/**
 * Acquires an exclusive lock using the PID file itself (synchronously with busy-wait)
 * Returns 0 if lock was acquired (need to spawn new process), or the existing PID if process is running
 */
function parsePidFile(content: string): { pid: number; version: number } {
	const lines = content.trim().split('\n');
	const pid = Number.parseInt(lines[0], 10);
	const version = lines.length > 1 ? parseInt(lines[1], 10) : 0;
	return { pid, version };
}

function acquirePidFileLock(
	pidFilePath: string,
	requestedVersion?: number,
	maxRetries = 100,
	retryDelay = 5
): { pid: number; version: number } {
	for (let attempt = 0; attempt < maxRetries; attempt++) {
		try {
			// Try to open exclusively - 'wx' fails if file exists
			const fd = openSync(pidFilePath, 'wx');
			closeSync(fd);
			return { pid: 0, version: 0 }; // Successfully acquired lock (file created), caller should spawn process
		} catch (err) {
			if (err.code === 'EEXIST') {
				// File exists - check if it contains a valid running process
				try {
					const pidContent = readFileSync(pidFilePath, 'utf-8');
					const { pid: existingPid, version: existingVersion } = parsePidFile(pidContent);

					if (!isNaN(existingPid) && isProcessRunning(existingPid)) {
						// If the version isn't the one we want, kill the existing process and re-acquire
						if (requestedVersion != null && requestedVersion !== existingVersion) {
							try {
								process.kill(existingPid);
							} catch {
								// Process may have already exited
							}
							try {
								unlinkSync(pidFilePath);
							} catch {
								// Another thread may have removed it
							}
							// Retry to acquire the lock for the new version
							const start = Date.now();
							while (Date.now() - start < retryDelay) {
								// Busy wait for process cleanup
							}
							continue;
						}
						// Valid process is running at same or higher version, return its PID
						return { pid: existingPid, version: existingVersion };
					}

					// Invalid/empty PID - check file age to determine if it's stale or being written
					const stats = statSync(pidFilePath);
					const fileAge = Date.now() - stats.mtimeMs;

					// If file is very new (less than 100ms) and empty/invalid, another thread is likely still writing to it
					if (fileAge < 100) {
						// Just wait and retry, don't try to remove
					} else {
						// Stale PID file (old and invalid), try to remove it
						try {
							unlinkSync(pidFilePath);
						} catch {
							// Another thread may have removed it, retry
						}
					}
				} catch {
					// Couldn't read/stat file, another thread might be modifying it, retry
				}

				// Wait a bit before retrying
				const start = Date.now();
				while (Date.now() - start < retryDelay) {
					// Busy wait
				}
			} else {
				throw err;
			}
		}
	}

	throw new Error(`Failed to acquire PID file lock after ${maxRetries} attempts`);
}

function createSpawn(spawnFunction: (...args: any) => child_process.ChildProcess, alwaysAllow?: boolean) {
	const basePath = env.getHdbBasePath();
	return function (command: string, args?: any, options?: any, callback?: (...args: any[]) => void) {
		if (!ALLOWED_COMMANDS.has(command.split(' ')[0]) && !alwaysAllow) {
			throw new Error(`Command ${command} is not allowed`);
		}
		const processName = options?.name;
		if (!processName)
			throw new Error(
				`Calling ${spawnFunction.name} in Harper must have a process "name" in the options to ensure that a single process is started and reused`
			);
		const requestedVersion = options?.version;

		// Ensure PID directory exists
		const pidDir = join(basePath, 'pids');
		mkdirSync(pidDir, { recursive: true });

		const pidFilePath = join(pidDir, `${processName}.pid`);

		// Try to acquire lock - returns pid: 0 if acquired, or existing PID/version
		const existing = acquirePidFileLock(pidFilePath, requestedVersion);

		if (existing.pid !== 0) {
			// Existing process is running, return wrapper
			return new ExistingProcessWrapper(existing.pid);
		}

		// We acquired the lock (file was created), spawn new process
		const childProcess = spawnFunction(command, args, options, callback);

		// Write PID (and version if provided) to the file we just created
		const pidFileContent =
			requestedVersion != null ? `${childProcess.pid}\n${requestedVersion}` : childProcess.pid.toString();
		try {
			writeFileSync(pidFilePath, pidFileContent, 'utf-8');
		} catch (err) {
			// Failed to write PID, clean up
			try {
				childProcess.kill();
				unlinkSync(pidFilePath);
			} catch {}
			throw err;
		}

		// Clean up PID file when process exits
		childProcess.on('exit', () => {
			try {
				unlinkSync(pidFilePath);
			} catch {
				// File may already be removed
			}
		});

		return childProcess;
	};
}

/**
 * Validates whether a module can be loaded based on security restrictions and returns the module path or replacement.
 * For file URLs, ensures the module is within the allowed path.
 * For node built-in modules, checks against an allowlist and returns any replacements.
 *
 * @param {string} moduleUrl - The URL or identifier of the module to be loaded, which may be a file: URL, node: URL, or bare module specifier.
 * @param {string} allowedPath - The absolute path that the module is allowed to load from.
 * @return {any} Returns undefined for allowed file paths, or a replacement module identifier for allowed node built-in modules.
 * @throws {Error} Throws an error if the module is outside the allowed path or if the module is not in the allowed list.
 */
function checkAllowedModulePath(moduleUrl: string, allowedPath?: string): boolean {
	if (moduleUrl.startsWith('file:')) {
		let path = fileURLToPath(moduleUrl);
		try {
			path = realpathSync(path);
		} catch {}
		if (!allowedPath || path.startsWith(allowedPath)) {
			return;
		}
		throw new Error(`Can not load module at ${path} outside of allowed path ${allowedPath}`);
	}
	let simpleName = moduleUrl.startsWith('node:') ? moduleUrl.slice(5) : moduleUrl;
	simpleName = simpleName.split('/')[0];
	if (ALLOWED_NODE_BUILTIN_MODULES.has(simpleName)) return REPLACED_BUILTIN_MODULES[simpleName];
	throw new Error(`Module ${moduleUrl} is not allowed to be imported`);
}

export function getContext() {
	return contextStorage.getStore() ?? {};
}
export function getUser() {
	return contextStorage.getStore()?.user;
}
export function getResponse() {
	return (contextStorage.getStore() as any)?.response;
}

export function preventFunctionConstructor() {
	Function.prototype.constructor = function () {}; // prevent this from being used to eval data in a parent context
}

function freezeIntrinsics() {
	overridableProperty(Object.prototype, 'toString');
	overridableProperty(Object.prototype, 'valueOf');
	overridableProperty(Object.prototype, 'hasOwnProperty');
	overridableProperty(Object.prototype, 'constructor');
	overridableProperty(Promise.prototype, 'then');
	overridableProperty(Date, 'now');
	for (let name of ['get', 'set', 'has', 'delete', 'clear', 'forEach', 'entries', 'keys', 'values']) {
		overridableProperty(Map.prototype, name);
	}
	for (let Intrinsic of [
		Object,
		Array,
		Promise,
		BigInt,
		String,
		Number,
		Boolean,
		Symbol,
		RegExp,
		Date,
		Map,
		Set,
		WeakMap,
		WeakSet,
		Math,
		JSON,
		Reflect,
		Atomics,
		SharedArrayBuffer,
		WeakRef,
		FinalizationRegistry,
	]) {
		Object.freeze(Intrinsic);
		Object.freeze((Intrinsic as any).prototype);
	}
	Object.freeze(Function);
}

/**
 * This can redefine a property into a getter/setter that will allow derivatives of a prototype to assign
 * a value to the property without incurring an error from the property being frozen and readonly.
 * @param target
 * @param name
 * @param value
 */
function overridableProperty(target, name, value = target[name]) {
	Object.defineProperty(target, name, {
		get() {
			return value;
		},
		set(value) {
			Object.defineProperty(this, name, {
				value,
				configurable: true,
				enumerable: true,
				writable: true,
			});
		},
	});
}
