import { log } from "./log.js";
import { defu } from "defu";
import { dirname, relative, resolve } from "node:path";
import is from "@sindresorhus/is";
import { findWorkspaces } from "find-workspaces";
import { existsSync } from "node:fs";
import picomatch from "picomatch";
import { exec } from "tinyexec";
import { escapePath, glob } from "tinyglobby";
//#region src/lib/file-matching.ts
const matchCache = /* @__PURE__ */ new Map();
const workspaceCache = /* @__PURE__ */ new Map();
/**
* Clear the memoized file tree and workspace caches. Call between test runs
* or when the same path needs to be re-scanned.
*/
function resetMatchCache() {
	matchCache.clear();
	workspaceCache.clear();
}
const DEFAULT_IGNORE = [
	"**/node_modules/**",
	"**/dist/**",
	"**/build/**",
	"**/coverage/**",
	"**/.DS_Store"
];
/**
* Get the full recursive file tree for a directory, memoized by path + respectIgnored.
* Returns relative POSIX paths (internal to tinyglobby; callers receive absolute paths via getMatches).
*/
async function getTree(path, respectIgnored) {
	const key = `${path}\0${respectIgnored ? "1" : "0"}`;
	let tree = matchCache.get(key);
	if (!tree) {
		let ignore = [];
		if (respectIgnored) try {
			const { stdout } = await exec("git", [
				"ls-files",
				"--others",
				"--ignored",
				"--exclude-standard",
				"--directory"
			], { nodeOptions: {
				cwd: path,
				stdio: [
					"ignore",
					"pipe",
					"ignore"
				]
			} });
			ignore = stdout.split("\n").filter(Boolean).map((p) => {
				const escaped = escapePath(p);
				return escaped.endsWith("/") ? `${escaped}**` : escaped;
			});
		} catch {
			ignore = [...DEFAULT_IGNORE];
		}
		tree = await glob("**", {
			cwd: path,
			dot: true,
			ignore
		});
		matchCache.set(key, tree);
	}
	return tree;
}
function validateWorkspaces(directory, workspaces) {
	const seen = /* @__PURE__ */ new Set();
	const validated = [];
	for (const workspace of workspaces) {
		if (!is.nonEmptyString(workspace)) {
			log.warn(`Skipping invalid workspace: expected non-empty string, got ${is(workspace)}`);
			continue;
		}
		const absolute = resolve(directory, workspace);
		if (absolute === directory) continue;
		if (!absolute.startsWith(directory)) {
			log.warn(`Skipping workspace "${workspace}": must be a child of "${directory}"`);
			continue;
		}
		if (!existsSync(absolute)) {
			log.warn(`Skipping workspace "${workspace}": path does not exist`);
			continue;
		}
		if (seen.has(absolute)) {
			log.warn(`Skipping workspace "${workspace}": duplicate entry`);
			continue;
		}
		seen.add(absolute);
		validated.push(absolute);
	}
	return validated;
}
/**
* Get workspace locations for a directory, memoized by directory path.
* Returns all found workspace location paths as absolute paths.
*
* Directories to any monorepo workspaces... only supports yarn, npm, pnpm, lerna, and bolt at the moment.
* Never includes the root path!
* @param directory - The root directory to search from
* @param workspaces - `false` to disable, `true` to auto-discover, `string[]` for a manual list
*/
function getWorkspaces(directory, workspaces = true) {
	if (workspaces === false) return [];
	let locations = workspaceCache.get(directory);
	if (!locations) {
		locations = validateWorkspaces(directory, workspaces === true ? findWorkspaces(directory, { stopDir: dirname(directory) })?.map((value) => value.location) ?? [] : workspaces);
		workspaceCache.set(directory, locations);
	}
	return locations;
}
const DEFAULT_MATCH_OPTIONS = {
	path: ".",
	recursive: false,
	respectIgnored: true,
	workspaces: true
};
/**
* Find files matching glob patterns in a directory's file tree.
*
* - Memoizes the file tree internally (keyed by path + respectIgnored)
* - Auto-prepends `**\/` to patterns when `options.recursive` is true
* - Always uses case-insensitive matching
* - When `options.workspaces` is set, also matches files in workspace directories dynamically.
* @param options - Must include `path`; optionally `recursive`, `respectIgnored`, and `workspaces`
* @param patterns - Root-relative glob patterns (e.g. `['package.json']`, `['*.gemspec']`)
* @param patternsRecursive - Optionally explicitly specify recursive pattern
* variation, otherwise  `**\/` is prepended automatically
*/
async function getMatches(options, patterns, patternsRecursive) {
	const resolved = defu(options, DEFAULT_MATCH_OPTIONS);
	const tree = await getTree(resolved.path, resolved.respectIgnored);
	let effectivePatterns;
	if (resolved.recursive) effectivePatterns = patternsRecursive ?? patterns.map((p) => `**/${p}`);
	else {
		effectivePatterns = [...patterns];
		if (resolved.workspaces) {
			const workspacePaths = getWorkspaces(resolved.path, resolved.workspaces);
			for (const workspace of workspacePaths) {
				const relativeWorkspace = relative(resolved.path, workspace).replaceAll("\\", "/");
				effectivePatterns.push(...patterns.map((p) => `${relativeWorkspace}/${p}`));
			}
		}
	}
	const isMatch = picomatch(effectivePatterns, { nocase: true });
	const results = [];
	for (const filePath of tree) if (isMatch(filePath)) results.push(resolve(resolved.path, filePath));
	return results.toSorted((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b));
}
//#endregion
export { getMatches, getTree, getWorkspaces, resetMatchCache };
