import { getLogger } from "../../../logger.js";
import { FUNCTION_TOOL_EXECUTION_TIMEOUT_MS_DEFAULT, FUNCTION_TOOL_SANDBOX_CLEANUP_INTERVAL_MS, FUNCTION_TOOL_SANDBOX_MAX_OUTPUT_SIZE_BYTES, FUNCTION_TOOL_SANDBOX_MAX_USE_COUNT, FUNCTION_TOOL_SANDBOX_POOL_TTL_MS, FUNCTION_TOOL_SANDBOX_QUEUE_WAIT_TIMEOUT_MS } from "../constants/execution-limits/index.js";
import { createExecutionWrapper, parseExecutionResult } from "./sandbox-utils.js";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createHash, randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { tmpdir } from "node:os";

//#region src/domains/run/tools/NativeSandboxExecutor.ts
/**
* NativeSandboxExecutor - Function Tool Execution Engine
* ========================================================
*
* Executes user-defined function tools in isolated sandboxes using native Node.js processes.
* The main challenge here is that we can't just eval() user code - that's a security nightmare.
* Instead, we spin up separate Node.js processes with their own dependency trees.
*
* The tricky part is making this fast. Installing deps every time would be brutal
* (2-5s per execution), so we cache sandboxes based on their dependency fingerprint.
*
* How it works:
*
* 1. User calls a function tool
* 2. We hash the dependencies (e.g., "axios@1.6.0,lodash@4.17.21")
* 3. Check if we already have a sandbox with those deps installed
* 4. If yes: reuse it. If no: create new one, install deps, cache it
* 5. Write the user's function code to a temp file
* 6. Execute it in the sandboxed process with resource limits
* 7. Return the result
*
* Sandbox lifecycle:
* - Created when first needed for a dependency set
* - Reused up to 50 times or 5 minutes, whichever comes first
* - Automatically cleaned up when expired
* - Failed sandboxes are immediately destroyed
*
* Security stuff:
* - Each execution runs in its own process (not just a function call)
* - Output limited to 1MB to prevent memory bombs
* - Timeouts with graceful SIGTERM, then SIGKILL if needed
* - Runs as non-root when possible
* - Uses OS temp directory so it gets cleaned up automatically
*
* Performance:
* - Cold start: ~100-500ms (vs 2-5s without caching)
* - Hot path: ~50-100ms (just execution, no install)
* - Memory bounded by pool size limits
*
* Deployment notes:
* - Uses /tmp on Linux/macOS, %TEMP% on Windows
* - Works in Docker, Kubernetes, serverless (Vercel, Lambda)
* - No files left in project directory (no git pollution)
*
* Note: In this repo, sandbox pooling is scoped to a single message/session via SandboxExecutorFactory.
*/
const logger = getLogger("native-sandbox-executor");
/**
* Semaphore for limiting concurrent executions based on vCPU allocation
*/
var ExecutionSemaphore = class {
	permits;
	waitQueue = [];
	maxWaitTime;
	constructor(permits, maxWaitTimeMs = FUNCTION_TOOL_SANDBOX_QUEUE_WAIT_TIMEOUT_MS) {
		this.permits = Math.max(1, permits);
		this.maxWaitTime = maxWaitTimeMs;
	}
	async acquire(fn) {
		await new Promise((resolve$1, reject) => {
			if (this.permits > 0) {
				this.permits--;
				resolve$1();
				return;
			}
			const timeoutId = setTimeout(() => {
				const index = this.waitQueue.findIndex((item) => item.resolve === resolve$1);
				if (index !== -1) {
					this.waitQueue.splice(index, 1);
					reject(/* @__PURE__ */ new Error(`Function execution queue timeout after ${this.maxWaitTime}ms. Too many concurrent executions.`));
				}
			}, this.maxWaitTime);
			this.waitQueue.push({
				resolve: () => {
					clearTimeout(timeoutId);
					this.permits--;
					resolve$1();
				},
				reject
			});
		});
		try {
			return await fn();
		} finally {
			this.permits++;
			const next = this.waitQueue.shift();
			if (next) next.resolve();
		}
	}
	getAvailablePermits() {
		return this.permits;
	}
	getQueueLength() {
		return this.waitQueue.length;
	}
};
var NativeSandboxExecutor = class {
	tempDir;
	sandboxPool = {};
	executionSemaphores = /* @__PURE__ */ new Map();
	poolCleanupInterval = null;
	sandboxInitPromises = /* @__PURE__ */ new Map();
	constructor() {
		this.tempDir = join(tmpdir(), "inkeep-sandboxes");
		this.ensureTempDir();
		this.startPoolCleanup();
	}
	async cleanup() {
		if (this.poolCleanupInterval) {
			clearInterval(this.poolCleanupInterval);
			this.poolCleanupInterval = null;
		}
		this.sandboxInitPromises.clear();
		for (const sandbox of Object.values(this.sandboxPool)) this.cleanupSandbox(sandbox.sandboxDir);
		this.sandboxPool = {};
	}
	getSemaphore(vcpus) {
		const effectiveVcpus = Math.max(1, vcpus || 1);
		if (!this.executionSemaphores.has(effectiveVcpus)) {
			logger.debug({ vcpus: effectiveVcpus }, "Creating new execution semaphore");
			this.executionSemaphores.set(effectiveVcpus, new ExecutionSemaphore(effectiveVcpus));
		}
		const semaphore = this.executionSemaphores.get(effectiveVcpus);
		if (!semaphore) throw new Error(`Failed to create semaphore for ${effectiveVcpus} vCPUs`);
		return semaphore;
	}
	getExecutionStats() {
		const stats = {};
		for (const [vcpus, semaphore] of this.executionSemaphores.entries()) stats[`vcpu_${vcpus}`] = {
			availablePermits: semaphore.getAvailablePermits(),
			queueLength: semaphore.getQueueLength()
		};
		return stats;
	}
	ensureTempDir() {
		try {
			mkdirSync(this.tempDir, { recursive: true });
		} catch {}
	}
	generateDependencyHash(dependencies) {
		const sortedDeps = Object.keys(dependencies).sort().map((key) => `${key}@${dependencies[key]}`).join(",");
		return createHash("sha256").update(sortedDeps).digest("hex").substring(0, 16);
	}
	getCachedSandbox(dependencyHash) {
		const poolKey = dependencyHash;
		const sandbox = this.sandboxPool[poolKey];
		if (sandbox && existsSync(sandbox.sandboxDir)) {
			const now = Date.now();
			if (now - sandbox.lastUsed < FUNCTION_TOOL_SANDBOX_POOL_TTL_MS && sandbox.useCount < FUNCTION_TOOL_SANDBOX_MAX_USE_COUNT) {
				sandbox.lastUsed = now;
				sandbox.useCount++;
				sandbox.activeCount++;
				logger.debug({
					poolKey,
					useCount: sandbox.useCount,
					sandboxDir: sandbox.sandboxDir,
					lastUsed: new Date(sandbox.lastUsed),
					activeCount: sandbox.activeCount
				}, "Reusing cached sandbox");
				return sandbox.sandboxDir;
			}
			if (sandbox.activeCount === 0) {
				this.cleanupSandbox(sandbox.sandboxDir);
				delete this.sandboxPool[poolKey];
			}
		}
		return null;
	}
	addToPool(dependencyHash, sandboxDir, dependencies) {
		const poolKey = dependencyHash;
		if (this.sandboxPool[poolKey]) this.cleanupSandbox(this.sandboxPool[poolKey].sandboxDir);
		this.sandboxPool[poolKey] = {
			sandboxDir,
			lastUsed: Date.now(),
			useCount: 1,
			dependencies,
			activeCount: 0
		};
		logger.debug({
			poolKey,
			sandboxDir
		}, "Added sandbox to pool");
	}
	cleanupSandbox(sandboxDir) {
		try {
			rmSync(sandboxDir, {
				recursive: true,
				force: true
			});
			logger.debug({ sandboxDir }, "Cleaned up sandbox");
		} catch (error) {
			logger.warn({
				sandboxDir,
				error
			}, "Failed to clean up sandbox");
		}
	}
	startPoolCleanup() {
		this.poolCleanupInterval = setInterval(() => {
			const now = Date.now();
			const keysToDelete = [];
			for (const [key, sandbox] of Object.entries(this.sandboxPool)) if (sandbox.activeCount === 0 && (now - sandbox.lastUsed > FUNCTION_TOOL_SANDBOX_POOL_TTL_MS || sandbox.useCount >= FUNCTION_TOOL_SANDBOX_MAX_USE_COUNT)) {
				this.cleanupSandbox(sandbox.sandboxDir);
				keysToDelete.push(key);
			}
			keysToDelete.forEach((key) => {
				delete this.sandboxPool[key];
			});
			if (keysToDelete.length > 0) logger.debug({ cleanedCount: keysToDelete.length }, "Cleaned up expired sandboxes");
		}, FUNCTION_TOOL_SANDBOX_CLEANUP_INTERVAL_MS);
	}
	detectModuleType(executeCode, configuredRuntime) {
		const esmPatterns = [
			/import\s+.*\s+from\s+['"]/g,
			/import\s*\(/g,
			/export\s+(default|const|let|var|function|class)/g,
			/export\s*\{/g
		];
		const cjsPatterns = [
			/require\s*\(/g,
			/module\.exports/g,
			/exports\./g
		];
		const hasEsmSyntax = esmPatterns.some((pattern) => pattern.test(executeCode));
		const hasCjsSyntax = cjsPatterns.some((pattern) => pattern.test(executeCode));
		if (configuredRuntime === "typescript") return hasCjsSyntax ? "cjs" : "esm";
		if (hasEsmSyntax && hasCjsSyntax) {
			logger.warn({ executeCode: `${executeCode.substring(0, 100)}...` }, "Both ESM and CommonJS syntax detected, defaulting to ESM");
			return "esm";
		}
		if (hasEsmSyntax) return "esm";
		if (hasCjsSyntax) return "cjs";
		return "cjs";
	}
	async executeFunctionTool(toolId, args, config) {
		const vcpus = config.sandboxConfig?.vcpus || 1;
		const semaphore = this.getSemaphore(vcpus);
		logger.debug({
			toolId,
			vcpus,
			availablePermits: semaphore.getAvailablePermits(),
			queueLength: semaphore.getQueueLength(),
			sandboxConfig: config.sandboxConfig,
			poolSize: Object.keys(this.sandboxPool).length
		}, "Acquiring execution slot for function tool");
		return semaphore.acquire(async () => {
			return this.executeInSandbox_Internal(toolId, args, config);
		});
	}
	async executeInSandbox_Internal(toolId, args, config) {
		const dependencies = config.dependencies || {};
		const dependencyHash = this.generateDependencyHash(dependencies);
		logger.debug({
			toolId,
			dependencies,
			dependencyHash,
			sandboxConfig: config.sandboxConfig,
			poolSize: Object.keys(this.sandboxPool).length
		}, "Executing function tool");
		const sandboxDir = await (async () => {
			const cachedDir = this.getCachedSandbox(dependencyHash);
			if (cachedDir) return cachedDir;
			const existingInit = this.sandboxInitPromises.get(dependencyHash);
			if (existingInit) {
				const dir = await existingInit;
				const poolEntry = this.sandboxPool[dependencyHash];
				if (poolEntry) poolEntry.activeCount++;
				return dir;
			}
			const initPromise = (async () => {
				const dir = join(this.tempDir, `sandbox-${dependencyHash}-${Date.now()}`);
				mkdirSync(dir, { recursive: true });
				logger.debug({
					toolId,
					dependencyHash,
					sandboxDir: dir,
					dependencies
				}, "Creating new sandbox");
				const packageJson = {
					name: `function-tool-${toolId}`,
					version: "1.0.0",
					dependencies
				};
				writeFileSync(join(dir, "package.json"), JSON.stringify(packageJson, null, 2), "utf8");
				if (Object.keys(dependencies).length > 0) await this.installDependencies(dir);
				this.addToPool(dependencyHash, dir, dependencies);
				this.sandboxPool[dependencyHash].activeCount++;
				return dir;
			})();
			this.sandboxInitPromises.set(dependencyHash, initPromise);
			try {
				return await initPromise;
			} finally {
				this.sandboxInitPromises.delete(dependencyHash);
			}
		})();
		let runDir = null;
		try {
			const moduleType = this.detectModuleType(config.executeCode, config.sandboxConfig?.runtime);
			const executionCode = createExecutionWrapper(config.executeCode, args);
			runDir = join(sandboxDir, "runs", `${Date.now()}-${createHash("sha256").update(randomUUID()).digest("hex").substring(0, 8)}`);
			mkdirSync(runDir, { recursive: true });
			writeFileSync(join(runDir, `index.${moduleType === "esm" ? "mjs" : "cjs"}`), executionCode, "utf8");
			return await this.executeInSandbox(runDir, config.sandboxConfig?.timeout || FUNCTION_TOOL_EXECUTION_TIMEOUT_MS_DEFAULT, moduleType, config.sandboxConfig);
		} finally {
			if (runDir) try {
				rmSync(runDir, {
					recursive: true,
					force: true
				});
			} catch {}
			const poolEntry = this.sandboxPool[dependencyHash];
			if (poolEntry) poolEntry.activeCount = Math.max(0, poolEntry.activeCount - 1);
		}
	}
	async installDependencies(sandboxDir) {
		return new Promise((resolve$1, reject) => {
			const npm = spawn("npm", [
				"install",
				"--no-audit",
				"--no-fund"
			], {
				cwd: sandboxDir,
				stdio: "pipe",
				env: {
					...process.env,
					npm_config_cache: join(sandboxDir, ".npm-cache"),
					npm_config_logs_dir: join(sandboxDir, ".npm-logs"),
					npm_config_tmp: join(sandboxDir, ".npm-tmp"),
					HOME: sandboxDir,
					npm_config_update_notifier: "false",
					npm_config_progress: "false",
					npm_config_loglevel: "error"
				}
			});
			let stderr = "";
			npm.stdout?.on("data", () => {});
			npm.stderr?.on("data", (data) => {
				stderr += data.toString();
			});
			npm.on("close", (code) => {
				if (code === 0) {
					logger.debug({ sandboxDir }, "Dependencies installed successfully");
					resolve$1();
				} else {
					logger.error({
						sandboxDir,
						code,
						stderr
					}, "Failed to install dependencies");
					reject(/* @__PURE__ */ new Error(`npm install failed with code ${code}: ${stderr}`));
				}
			});
			npm.on("error", (err) => {
				logger.error({
					sandboxDir,
					error: err
				}, "Failed to spawn npm install");
				reject(err);
			});
		});
	}
	async executeInSandbox(sandboxDir, timeout, moduleType, _sandboxConfig) {
		return new Promise((resolve$1, reject) => {
			const fileExtension = moduleType === "esm" ? "mjs" : "cjs";
			const spawnOptions = {
				cwd: sandboxDir,
				stdio: "pipe",
				uid: process.getuid ? process.getuid() : void 0,
				gid: process.getgid ? process.getgid() : void 0
			};
			const node = spawn("node", [`index.${fileExtension}`], spawnOptions);
			let stdout = "";
			let stderr = "";
			let outputSize = 0;
			node.stdout?.on("data", (data) => {
				const dataStr = data.toString();
				outputSize += dataStr.length;
				if (outputSize > FUNCTION_TOOL_SANDBOX_MAX_OUTPUT_SIZE_BYTES) {
					node.kill("SIGTERM");
					reject(/* @__PURE__ */ new Error(`Output size exceeded limit of ${FUNCTION_TOOL_SANDBOX_MAX_OUTPUT_SIZE_BYTES} bytes`));
					return;
				}
				stdout += dataStr;
			});
			node.stderr?.on("data", (data) => {
				const dataStr = data.toString();
				outputSize += dataStr.length;
				if (outputSize > FUNCTION_TOOL_SANDBOX_MAX_OUTPUT_SIZE_BYTES) {
					node.kill("SIGTERM");
					reject(/* @__PURE__ */ new Error(`Output size exceeded limit of ${FUNCTION_TOOL_SANDBOX_MAX_OUTPUT_SIZE_BYTES} bytes`));
					return;
				}
				stderr += dataStr;
			});
			const timeoutId = setTimeout(() => {
				logger.warn({
					sandboxDir,
					timeout
				}, "Function execution timed out, killing process");
				node.kill("SIGTERM");
				const forceKillTimeout = Math.min(Math.max(timeout / 10, 2e3), 5e3);
				setTimeout(() => {
					try {
						node.kill("SIGKILL");
					} catch {}
				}, forceKillTimeout);
				reject(/* @__PURE__ */ new Error(`Function execution timed out after ${timeout}ms`));
			}, timeout);
			node.on("close", (code, signal) => {
				clearTimeout(timeoutId);
				if (code === 0) try {
					const result = parseExecutionResult(stdout, "function", logger);
					if (typeof result === "object" && result !== null && "success" in result) {
						const parsed = result;
						if (parsed.success) resolve$1(parsed.result);
						else reject(new Error(parsed.error || "Function execution failed"));
					} else resolve$1(result);
				} catch (parseError) {
					logger.error({
						stdout,
						stderr,
						parseError
					}, "Failed to parse function result");
					reject(/* @__PURE__ */ new Error(`Invalid function result: ${stdout}`));
				}
				else {
					const errorMsg = signal ? `Function execution killed by signal ${signal}: ${stderr}` : `Function execution failed with code ${code}: ${stderr}`;
					logger.error({
						code,
						signal,
						stderr
					}, "Function execution failed");
					reject(new Error(errorMsg));
				}
			});
			node.on("error", (error) => {
				clearTimeout(timeoutId);
				logger.error({
					sandboxDir,
					error
				}, "Failed to spawn node process");
				reject(error);
			});
		});
	}
};

//#endregion
export { NativeSandboxExecutor };