import { copyFile, mkdir, readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import string from "@poppinss/utils/string";
import { relative } from "node:path/posix";
import Cache from "tmp-cache";
import { isJunk } from "junk";
import fastGlob from "fast-glob";
import Hooks from "@poppinss/hooks";
import { existsSync } from "node:fs";
import getRandomPort from "get-port";
import { execa, execaNode } from "execa";
import { importDefault, naturalSort } from "@poppinss/utils";
import { EnvLoader, EnvParser } from "@adonisjs/env";
import chokidar from "chokidar";
import { parseTsconfig } from "get-tsconfig";
import { basename, dirname as dirname$1, isAbsolute, join as join$1, relative as relative$1 } from "node:path";
import { debuglog } from "node:util";
import { fdir } from "fdir";
import lodash from "@poppinss/utils/lodash";
import { Lang, parse } from "@ast-grep/napi";
import picomatch from "picomatch";
//#region src/debug.ts
/**
* Debug logger for the AdonisJS assembler package.
*
* This debug instance is configured to log messages under the 'adonisjs:assembler'
* namespace. Debug messages are only shown when the NODE_DEBUG environment variable
* includes 'adonisjs:assembler'.
*
* @example
* // Enable debug logging
* // NODE_DEBUG=adonisjs:assembler node ace serve
* debug('Starting development server...')
*/
var debug_default = debuglog("adonisjs:assembler");
//#endregion
//#region src/utils.ts
/**
* Default set of args to pass in order to run TypeScript
* source. Used by "run" and "runNode" scripts
*/
const DEFAULT_NODE_ARGS = ["--import=@poppinss/ts-exec", "--enable-source-maps"];
/**
* Parses tsconfig.json and prints errors using typescript compiler host
*
* This function reads and parses the tsconfig.json file from the given directory,
* handling diagnostic errors and returning a parsed configuration that can be
* used by other TypeScript operations.
*
* @deprecated While we are experimenting with the readTsConfig method
*
* @param cwd - The current working directory URL or string path
* @param ts - TypeScript module reference
* @returns Parsed TypeScript configuration or undefined if parsing failed
*/
function parseConfig(cwd, ts, path = "tsconfig.json") {
	const cwdPath = typeof cwd === "string" ? cwd : fileURLToPath(cwd);
	const configFile = join$1(cwdPath, path);
	debug_default("parsing config file \"%s\"", configFile);
	let hardException = null;
	const parsedConfig = ts.getParsedCommandLineOfConfigFile(configFile, {}, {
		...ts.sys,
		useCaseSensitiveFileNames: true,
		getCurrentDirectory: () => cwdPath,
		onUnRecoverableConfigFileDiagnostic: (error) => hardException = error
	});
	if (hardException) {
		const compilerHost = ts.createCompilerHost({});
		console.log(ts.formatDiagnosticsWithColorAndContext([hardException], compilerHost));
		return;
	}
	if (parsedConfig.errors.length) {
		const compilerHost = ts.createCompilerHost({});
		console.log(ts.formatDiagnosticsWithColorAndContext(parsedConfig.errors, compilerHost));
		return;
	}
	if (parsedConfig.raw.include) parsedConfig.raw.include = parsedConfig.raw.include.map((includePath) => {
		return includePath.replace("${configDir}/", "");
	});
	if (parsedConfig.raw.exclude) parsedConfig.raw.exclude = parsedConfig.raw.exclude.map((excludePath) => {
		return excludePath.replace("${configDir}/", "");
	});
	return parsedConfig;
}
function readTsConfig(cwd) {
	const tsConfigPath = join$1(cwd, "tsconfig.json");
	debug_default("reading config file from location \"%s\"", tsConfigPath);
	try {
		const tsConfig = parseTsconfig(tsConfigPath);
		if (tsConfig.include) tsConfig.include = tsConfig.include.map((resolvedPath) => {
			return resolvedPath.replace(cwd, "");
		});
		if (tsConfig.exclude) tsConfig.exclude = tsConfig.exclude.map((resolvedPath) => {
			return resolvedPath.replace(cwd, "");
		});
		debug_default("read tsconfig %O", tsConfig);
		return {
			path: tsConfigPath,
			config: tsConfig
		};
	} catch {
		return null;
	}
}
/**
* Runs a Node.js script as a child process and inherits the stdio streams
*
* This function spawns a Node.js child process with TypeScript support enabled
* by default through ts-exec. It's primarily used for running development
* servers and test scripts.
*
* @param cwd - The current working directory URL or string path
* @param options - Script execution options including args, environment, etc.
* @returns Child process instance from execa
*/
function runNode(cwd, options) {
	return execaNode(options.script, options.scriptArgs, {
		nodeOptions: DEFAULT_NODE_ARGS.concat(options.nodeArgs),
		preferLocal: true,
		windowsHide: false,
		localDir: cwd,
		cwd,
		reject: options.reject ?? false,
		buffer: false,
		stdio: options.stdio || "inherit",
		env: {
			...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
			...options.env
		}
	});
}
/**
* Runs a script as a child process and inherits the stdio streams
*
* This function spawns a generic child process for running any executable
* script. Unlike runNode, this doesn't include TypeScript-specific Node.js arguments.
*
* @param cwd - The current working directory URL or string path
* @param options - Script execution options (excluding nodeArgs)
* @returns Child process instance from execa
*/
function run(cwd, options) {
	return execa(options.script, options.scriptArgs, {
		preferLocal: true,
		windowsHide: false,
		localDir: cwd,
		cwd,
		buffer: false,
		stdio: options.stdio || "inherit",
		env: {
			...options.stdio === "pipe" ? { FORCE_COLOR: "true" } : {},
			...options.env
		}
	});
}
/**
* Watches the file system using chokidar with the provided options
*
* Creates a file system watcher that monitors the current directory
* for changes, supporting various chokidar options for customization.
*
* @param options - Chokidar watch options
* @returns Chokidar FSWatcher instance
*/
function watch(options) {
	return chokidar.watch(["."], options);
}
/**
* Returns the port to use after inspecting the dot-env files inside
* a given directory.
*
* A random port is used when the specified port is in use. Following
* is the logic for finding a specified port:
*
* - The "process.env.PORT" value is used if exists.
* - The dot-env files are loaded using the "EnvLoader" and the PORT
*   value is used by iterating over all the loaded files. The
*   iteration stops after first find.
* - Falls back to port 3333 if no PORT is found in environment files.
*
* @param cwd - The current working directory URL
* @returns Promise resolving to an available port number
*/
async function getPort(cwd) {
	/**
	* Use existing port if exists
	*/
	if (process.env.PORT) return getRandomPort({ port: Number(process.env.PORT) });
	/**
	* Loop over files and use the port from their contents. Stops
	* after first match
	*/
	const files = await new EnvLoader(cwd).load();
	for (let file of files) {
		const envVariables = await new EnvParser(file.contents, cwd).parse();
		if (envVariables.PORT) return getRandomPort({ port: Number(envVariables.PORT) });
	}
	/**
	* Use 3333 as the port
	*/
	return getRandomPort({ port: 3333 });
}
/**
* Helper function to copy files from relative paths or glob patterns
*
* This function handles copying files and directories while preserving
* directory structure. It supports both direct file paths and glob patterns,
* and automatically filters out junk files.
*
* @param files - Array of file paths or glob patterns to copy
* @param cwd - Source directory path
* @param outDir - Destination directory path
* @returns Promise resolving when all files are copied
*/
async function copyFiles(files, cwd, outDir) {
	/**
	* Looping over files and create a new collection with paths
	* and glob patterns
	*/
	const { paths, patterns } = files.reduce((result, file) => {
		/**
		* If file is a glob pattern, then push it to patterns
		*/
		if (fastGlob.isDynamicPattern(file)) {
			result.patterns.push(file);
			return result;
		}
		/**
		* Otherwise, check if file exists and push it to paths to copy
		*/
		if (existsSync(join$1(cwd, file))) result.paths.push(file);
		return result;
	}, {
		patterns: [],
		paths: []
	});
	debug_default("copyFiles inputs: %O, paths: %O, patterns: %O", files, paths, patterns);
	/**
	* Getting list of relative paths from glob patterns
	*/
	const filePaths = paths.concat(await fastGlob(patterns, {
		cwd,
		dot: true
	})).filter((file) => {
		return !isJunk(basename(file));
	});
	/**
	* Finally copy files to the destination by keeping the same
	* directory structure and ignoring junk files
	*/
	debug_default("copying files %O to destination \"%s\"", filePaths, outDir);
	const copyPromises = filePaths.map(async (file) => {
		const src = isAbsolute(file) ? file : join$1(cwd, file);
		const dest = join$1(outDir, relative$1(cwd, src));
		await mkdir(dirname$1(dest), { recursive: true });
		return copyFile(src, dest);
	});
	return await Promise.all(copyPromises);
}
/**
* Memoize a function using an LRU cache. The function must accept
* only one argument as a string value.
*
* This utility provides caching for expensive function calls to improve
* performance by storing results in memory.
*
* @param fn - Function to memoize (only first argument is considered for memoization)
* @param maxKeys - Optional maximum number of cached keys
* @returns Memoized version of the function
*/
function memoize(fn, maxKeys) {
	const cache = new Cache({ max: maxKeys });
	return (input, ...args) => {
		if (cache.has(input)) return cache.get(input);
		return fn(input, ...args);
	};
}
/**
* Returns a boolean telling if the path value is a relative
* path starting with "./" or "../"
*
* @param pathValue - The path string to check
* @returns True if the path is relative, false otherwise
*/
function isRelative(pathValue) {
	return pathValue.startsWith("./") || pathValue.startsWith("../");
}
/**
* Imports a selected set of lazy hooks and creates an instance of the
* Hooks class
*
* This function dynamically imports and initializes hooks based on the
* provided configuration, supporting different types of hooks for various
* assembler operations.
*
* @param rcFileHooks - Hook configuration from the RC file
* @param names - Array of hook names to load
* @returns Promise resolving to configured Hooks instance
*/
async function loadHooks(rcFileHooks, names) {
	const groups = names.map((name) => {
		return {
			group: name,
			hooks: rcFileHooks?.[name] ?? []
		};
	});
	const hooks = new Hooks();
	for (const { group, hooks: collection } of groups) for (const item of collection) if ("run" in item) hooks.add(group, item.run);
	else hooks.add(group, await importDefault(item));
	return hooks;
}
/**
* Wraps a function inside another function that throttles the concurrent
* executions of a function. If the function is called too quickly, then
* it may result in two invocations at max.
*
* This utility prevents overwhelming the system with rapid successive calls
* by ensuring only one execution happens at a time, with at most one queued call.
*
* @param fn - Function to throttle
* @param name - Optional name for debugging purposes
* @returns Throttled version of the function
*/
function throttle(fn, name) {
	name = name || "throttled";
	let isBusy = false;
	let hasQueuedCalls = false;
	let lastCallArgs;
	async function throttled(...args) {
		if (isBusy) {
			debug_default("ignoring \"%s\" invocation as current execution is in progress", name);
			hasQueuedCalls = true;
			lastCallArgs = args;
			return;
		}
		isBusy = true;
		debug_default("executing throttled function \"%s\"", name);
		await fn(...args);
		debug_default("executed throttled function \"%s\"", name);
		isBusy = false;
		if (hasQueuedCalls) {
			hasQueuedCalls = false;
			debug_default("resuming and running latest \"%s\" invocation", name);
			await throttled(...lastCallArgs);
		}
	}
	return throttled;
}
/**
* Removes the file extension from a file path
*
* @param filePath - The file path with extension
* @returns The file path without extension
*/
function removeExtension(filePath) {
	return filePath.substring(0, filePath.lastIndexOf("."));
}
//#endregion
//#region src/virtual_file_system.ts
const BYPASS_FN = (input) => input;
const DEFAULT_GLOB = [
	"**/!(*.d).ts",
	"**/*.tsx",
	"**/*.js"
];
/**
* Virtual file system for managing and tracking files with AST parsing capabilities.
*
* The VirtualFileSystem provides an abstraction layer over the physical file system,
* allowing efficient file scanning, filtering, and AST parsing with caching. It's
* designed to work with TypeScript/JavaScript files and provides various output
* formats for different use cases.
*
* @example
* const vfs = new VirtualFileSystem('/src', { glob: ['**\/*.ts'] })
* await vfs.scan()
* const fileTree = vfs.asTree()
* const astNode = await vfs.get('/src/app.ts')
*/
var VirtualFileSystem = class {
	/**
	* Absolute path to the source directory from where to read the files.
	* Additionally, a glob pattern or a filter could be specified to
	* narrow down the scanned files list.
	*/
	#source;
	/**
	* Filesystem options
	*/
	#options;
	/**
	* Files collected from the initial scan with pre-computed relative paths
	*/
	#files = /* @__PURE__ */ new Map();
	/**
	* LRU cache storing parsed AST nodes by file path with size limit
	*/
	#astCache = new Cache({ max: 60 });
	/**
	* Matcher is defined when glob is defined via the options
	*/
	#matcher;
	/**
	* Picomatch options used for file pattern matching
	*/
	#picoMatchOptions;
	/**
	* Create a new VirtualFileSystem instance
	*
	* @param source - Absolute path to the source directory
	* @param options - Optional configuration for file filtering and processing
	*/
	constructor(source, options) {
		this.#source = source;
		this.#options = options ?? {};
		this.#picoMatchOptions = {
			cwd: this.#source,
			dot: true
		};
		this.#matcher = picomatch(this.#options.glob ?? DEFAULT_GLOB, this.#picoMatchOptions);
	}
	/**
	* Scans the filesystem to collect the files. Newly files must
	* be added via the ".add" method.
	*
	* This method performs an initial scan of the source directory using
	* the configured glob patterns and populates the internal file list.
	*/
	async scan() {
		debug_default("fetching entities from source \"%s\"", this.#source);
		const crawler = new fdir().globWithOptions(this.#options.glob ?? DEFAULT_GLOB, this.#picoMatchOptions).withFullPaths();
		if (this.#options.filter) crawler.filter(this.#options.filter);
		const filesList = await crawler.crawl(this.#source).withPromise();
		debug_default("scanned files %O", filesList);
		const sortedFiles = filesList.sort(naturalSort);
		this.#files.clear();
		for (let filePath of sortedFiles) {
			filePath = string.toUnixSlash(filePath);
			const relativePath = removeExtension(relative(this.#source, filePath));
			this.#files.set(filePath, relativePath);
		}
	}
	/**
	* Check if a given file is part of the virtual file system. The method
	* checks for the scanned files as well as glob pattern matches.
	*
	* @param filePath - Absolute file path to check
	* @returns True if the file is tracked or matches the configured patterns
	*/
	has(filePath) {
		/**
		* File is already tracked as part of the initial scan
		*/
		if (this.#files.has(filePath)) return true;
		/**
		* Return false if file is not within the source dir
		*/
		if (!filePath.startsWith(this.#source)) return false;
		/**
		* If a match exists, then check if the file matches via the
		* matcher test
		*/
		return this.#matcher(filePath);
	}
	/**
	* Returns the files as a flat list of key-value pairs
	*
	* Converts the tracked files into a flat object where keys are relative
	* paths (without extensions) and values are absolute file paths.
	*
	* @param options - Optional transformation functions for keys and values
	* @returns Object with file mappings
	*/
	asList(options) {
		const list = {};
		const transformKey = options?.transformKey ?? BYPASS_FN;
		const transformValue = options?.transformValue ?? BYPASS_FN;
		for (const [filePath, relativePath] of this.#files) list[transformKey(relativePath)] = transformValue(filePath);
		return list;
	}
	/**
	* Returns the files as a nested tree structure
	*
	* Converts the tracked files into a hierarchical object structure that
	* mirrors the directory structure of the source files.
	*
	* @param options - Optional transformation functions for keys and values
	* @returns Nested object representing the file tree
	*/
	asTree(options) {
		const list = {};
		const transformKey = options?.transformKey ?? BYPASS_FN;
		const transformValue = options?.transformValue ?? BYPASS_FN;
		for (const [filePath, relativePath] of this.#files) {
			const key = transformKey(relativePath);
			lodash.set(list, key.split("/"), transformValue(filePath, key));
		}
		return list;
	}
	/**
	* Add a new file to the virtual file system. File is only added when it
	* matches the pre-defined filters.
	*
	* @param filePath - Absolute path of the file to add
	* @returns True if the file was added, false if it doesn't match filters
	*/
	add(filePath) {
		if (this.has(filePath)) {
			debug_default("adding new \"%s\" file to the virtual file system", filePath);
			const relativePath = removeExtension(relative(this.#source, filePath));
			this.#files.set(filePath, relativePath);
			return true;
		}
		return false;
	}
	/**
	* Remove a file from the virtual file system
	*
	* @param filePath - Absolute path of the file to remove
	* @returns True if the file was removed, false if it wasn't tracked
	*/
	remove(filePath) {
		debug_default("removing \"%s\" file from virtual file system", filePath);
		return this.#files.delete(filePath);
	}
	/**
	* Returns the file contents as AST-grep node and caches it
	* forever. Use the "invalidate" method to remove it from the cache.
	*
	* This method reads the file content, parses it into an AST using ast-grep,
	* and caches the result for future requests to improve performance.
	*
	* @param filePath - The absolute path to the file to parse
	* @returns Promise resolving to the AST-grep node
	*/
	async get(filePath) {
		const cached = this.#astCache.get(filePath);
		if (cached) {
			debug_default("returning AST nodes from cache \"%s\"", filePath);
			return cached;
		}
		const fileContents = await readFile(filePath, "utf-8");
		debug_default("parsing \"%s\" file to AST", filePath);
		this.#astCache.set(filePath, parse(Lang.TypeScript, fileContents).root());
		return this.#astCache.get(filePath);
	}
	/**
	* Invalidates AST cache for a single file or all files
	*
	* Use this method when files have been modified to ensure fresh
	* AST parsing on subsequent get() calls.
	*
	* @param filePath - Optional file path to clear. If omitted, clears entire cache
	*/
	invalidate(filePath) {
		if (filePath) {
			debug_default("invalidate AST cache \"%s\"", filePath);
			this.#astCache.delete(filePath);
		} else {
			debug_default("clear AST cache");
			this.#astCache.clear();
		}
	}
	/**
	* Clear all scanned files from memory
	*
	* Removes all tracked files from the internal file list, effectively
	* resetting the virtual file system.
	*/
	clear() {
		this.#files.clear();
	}
};
//#endregion
export { loadHooks as a, readTsConfig as c, runNode as d, throttle as f, isRelative as i, removeExtension as l, debug_default as m, copyFiles as n, memoize as o, watch as p, getPort as r, parseConfig as s, VirtualFileSystem as t, run as u };
