import { formatValidationErrors, validateManifest } from "../validate/validate-manifest.js";
import fs from "node:fs";
import path from "node:path";
import { Command } from "commander";
import { Lang, parse } from "@ast-grep/napi";

//#region src/cli/commands/plugin/sync/sync.ts
/**
* Checks whether a resolved file path is within a given directory boundary.
* Uses path.resolve + startsWith to prevent directory traversal.
*
* @param filePath - The path to check (will be resolved to absolute)
* @param boundary - The directory that must contain filePath
* @returns true if filePath is inside boundary (or equal to it)
*/
function isWithinDirectory(filePath, boundary) {
	const resolvedPath = path.resolve(filePath);
	const resolvedBoundary = path.resolve(boundary);
	return resolvedPath === resolvedBoundary || resolvedPath.startsWith(`${resolvedBoundary}${path.sep}`);
}
/**
* Validates a parsed JSON object against the plugin-manifest JSON schema.
* Returns the manifest if valid, or null and logs schema errors.
*/
function validateManifestWithSchema(obj, sourcePath) {
	const result = validateManifest(obj);
	if (result.valid && result.manifest) return result.manifest;
	if (result.errors?.length) console.warn(`Warning: Manifest at ${sourcePath} failed schema validation:\n${formatValidationErrors(result.errors, obj)}`);
	return null;
}
/**
* Known packages that may contain AppKit plugins.
* Always scanned for manifests, even if not imported in the server file.
*/
const KNOWN_PLUGIN_PACKAGES = ["@databricks/appkit"];
/**
* Candidate paths for the server entry file, relative to cwd.
* Checked in order; the first that exists is used.
*/
const SERVER_FILE_CANDIDATES = ["server/server.ts", "server/index.ts"];
/**
* Find the server entry file by checking candidate paths in order.
*
* @param cwd - Current working directory
* @returns Absolute path to the server file, or null if none found
*/
function findServerFile(cwd) {
	for (const candidate of SERVER_FILE_CANDIDATES) {
		const fullPath = path.join(cwd, candidate);
		if (fs.existsSync(fullPath)) return fullPath;
	}
	return null;
}
/**
* Extract all named imports from the AST root using structural node traversal.
* Handles single/double quotes, multiline imports, and aliased imports.
*
* @param root - AST root node
* @returns Array of parsed imports with name, original name, and source
*/
function parseImports(root) {
	const imports = [];
	const importStatements = root.findAll({ rule: { kind: "import_statement" } });
	for (const stmt of importStatements) {
		const sourceNode = stmt.find({ rule: { kind: "string" } });
		if (!sourceNode) continue;
		const source = sourceNode.text().replace(/^['"]|['"]$/g, "");
		const namedImports = stmt.find({ rule: { kind: "named_imports" } });
		if (!namedImports) continue;
		const specifiers = namedImports.findAll({ rule: { kind: "import_specifier" } });
		for (const specifier of specifiers) {
			const children = specifier.children();
			if (children.length >= 3) {
				const originalName = children[0].text();
				const localName = children[children.length - 1].text();
				imports.push({
					name: localName,
					originalName,
					source
				});
			} else {
				const name = specifier.text();
				imports.push({
					name,
					originalName: name,
					source
				});
			}
		}
	}
	return imports;
}
/**
* Extract local names of plugins actually used in the `plugins: [...]` array
* passed to `createApp()`. Uses structural AST traversal to find `pair` nodes
* with key "plugins" and array values containing call expressions.
*
* @param root - AST root node
* @returns Set of local variable names used as plugin calls in the plugins array
*/
function parsePluginUsages(root) {
	const usedNames = /* @__PURE__ */ new Set();
	const pairs = root.findAll({ rule: { kind: "pair" } });
	for (const pair of pairs) {
		const key = pair.find({ rule: { kind: "property_identifier" } });
		if (!key || key.text() !== "plugins") continue;
		const arrayNode = pair.find({ rule: { kind: "array" } });
		if (!arrayNode) continue;
		for (const child of arrayNode.children()) if (child.kind() === "call_expression") {
			const callee = child.children()[0];
			if (callee?.kind() === "identifier") usedNames.add(callee.text());
		}
	}
	return usedNames;
}
/**
* File extensions to try when resolving a relative import to a file path.
*/
const RESOLVE_EXTENSIONS = [
	".ts",
	".tsx",
	".js",
	".jsx"
];
/**
* Resolve a relative import source to the plugin directory containing a manifest.json.
* Follows the convention that plugins live in their own directory with a manifest.json.
*
* Resolution strategy:
* 1. If the import path is a directory, look for manifest.json directly in it
* 2. If the import path + extension is a file, look for manifest.json in its parent directory
* 3. If the import path is a directory with an index file, look for manifest.json in that directory
*
* @param importSource - The relative import specifier (e.g. "./plugins/my-plugin")
* @param serverFileDir - Absolute path to the directory containing the server file
* @returns Absolute path to manifest.json, or null if not found
*/
function resolveLocalManifest(importSource, serverFileDir, projectRoot) {
	const resolved = path.resolve(serverFileDir, importSource);
	if (!isWithinDirectory(resolved, projectRoot || serverFileDir)) {
		console.warn(`Warning: Skipping import "${importSource}" — resolves outside the project directory`);
		return null;
	}
	if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
		const manifestPath = path.join(resolved, "manifest.json");
		if (fs.existsSync(manifestPath)) return manifestPath;
	}
	for (const ext of RESOLVE_EXTENSIONS) {
		const filePath = `${resolved}${ext}`;
		if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
			const dir = path.dirname(filePath);
			const manifestPath = path.join(dir, "manifest.json");
			if (fs.existsSync(manifestPath)) return manifestPath;
			break;
		}
	}
	for (const ext of RESOLVE_EXTENSIONS) {
		const indexPath = path.join(resolved, `index${ext}`);
		if (fs.existsSync(indexPath)) {
			const manifestPath = path.join(resolved, "manifest.json");
			if (fs.existsSync(manifestPath)) return manifestPath;
			break;
		}
	}
	return null;
}
/**
* Discover plugin manifests from local (relative) imports in the server file.
* Resolves each relative import to a directory and looks for manifest.json.
*
* @param relativeImports - Parsed imports with relative sources (starting with . or /)
* @param serverFileDir - Absolute path to the directory containing the server file
* @param cwd - Current working directory (for computing relative paths in output)
* @returns Map of plugin name to template plugin entry for local plugins
*/
function discoverLocalPlugins(relativeImports, serverFileDir, cwd) {
	const plugins = {};
	for (const imp of relativeImports) {
		const manifestPath = resolveLocalManifest(imp.source, serverFileDir, cwd);
		if (!manifestPath) continue;
		try {
			const content = fs.readFileSync(manifestPath, "utf-8");
			const manifest = validateManifestWithSchema(JSON.parse(content), manifestPath);
			if (!manifest) continue;
			const relativePath = path.relative(cwd, path.dirname(manifestPath));
			plugins[manifest.name] = {
				name: manifest.name,
				displayName: manifest.displayName,
				description: manifest.description,
				package: `./${relativePath}`,
				resources: manifest.resources
			};
		} catch (error) {
			console.warn(`Warning: Failed to parse manifest at ${manifestPath}:`, error instanceof Error ? error.message : error);
		}
	}
	return plugins;
}
/**
* Discover plugin manifests from a package's dist folder.
* Looks for manifest.json files in dist/plugins/{plugin-name}/ directories.
*
* @param packagePath - Path to the package in node_modules
* @returns Array of plugin manifests found in the package
*/
function discoverPluginManifests(packagePath) {
	const pluginsDir = path.join(packagePath, "dist", "plugins");
	const manifests = [];
	if (!fs.existsSync(pluginsDir)) return manifests;
	const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
	for (const entry of entries) if (entry.isDirectory()) {
		const manifestPath = path.join(pluginsDir, entry.name, "manifest.json");
		if (fs.existsSync(manifestPath)) try {
			const content = fs.readFileSync(manifestPath, "utf-8");
			const manifest = validateManifestWithSchema(JSON.parse(content), manifestPath);
			if (manifest) manifests.push(manifest);
		} catch (error) {
			console.warn(`Warning: Failed to parse manifest at ${manifestPath}:`, error instanceof Error ? error.message : error);
		}
	}
	return manifests;
}
/**
* Scan node_modules for packages with plugin manifests.
*
* @param cwd - Current working directory to search from
* @param packages - Set of npm package names to scan for plugin manifests
* @returns Map of plugin name to template plugin entry
*/
function scanForPlugins(cwd, packages) {
	const plugins = {};
	for (const packageName of packages) {
		const packagePath = path.join(cwd, "node_modules", packageName);
		if (!fs.existsSync(packagePath)) continue;
		const manifests = discoverPluginManifests(packagePath);
		for (const manifest of manifests) plugins[manifest.name] = {
			name: manifest.name,
			displayName: manifest.displayName,
			description: manifest.description,
			package: packageName,
			resources: manifest.resources
		};
	}
	return plugins;
}
/**
* Scan a directory for plugin manifests in direct subdirectories.
* Each subdirectory is expected to contain a manifest.json file.
* Used with --plugins-dir to discover plugins from source instead of node_modules.
*
* @param dir - Absolute path to the directory containing plugin subdirectories
* @param packageName - Package name to assign to discovered plugins
* @returns Map of plugin name to template plugin entry
*/
function scanPluginsDir(dir, packageName) {
	const plugins = {};
	if (!fs.existsSync(dir)) return plugins;
	const entries = fs.readdirSync(dir, { withFileTypes: true });
	for (const entry of entries) {
		if (!entry.isDirectory()) continue;
		const manifestPath = path.join(dir, entry.name, "manifest.json");
		if (!fs.existsSync(manifestPath)) continue;
		try {
			const content = fs.readFileSync(manifestPath, "utf-8");
			const manifest = validateManifestWithSchema(JSON.parse(content), manifestPath);
			if (manifest) plugins[manifest.name] = {
				name: manifest.name,
				displayName: manifest.displayName,
				description: manifest.description,
				package: packageName,
				resources: manifest.resources
			};
		} catch (error) {
			console.warn(`Warning: Failed to parse manifest at ${manifestPath}:`, error instanceof Error ? error.message : error);
		}
	}
	return plugins;
}
/**
* Write (or preview) the template plugins manifest to disk.
*/
function writeManifest(outputPath, { plugins }, options) {
	const templateManifest = {
		$schema: "https://databricks.github.io/appkit/schemas/template-plugins.schema.json",
		version: "1.0",
		plugins
	};
	if (options.write) {
		fs.writeFileSync(outputPath, `${JSON.stringify(templateManifest, null, 2)}\n`);
		if (!options.silent) console.log(`\n✓ Wrote ${outputPath}`);
	} else if (!options.silent) {
		console.log("\nTo write the manifest, run:");
		console.log("  npx appkit plugin sync --write\n");
		console.log("Preview:");
		console.log("─".repeat(60));
		console.log(JSON.stringify(templateManifest, null, 2));
		console.log("─".repeat(60));
	}
}
/**
* Run the plugin sync command.
* Parses the server entry file to discover which packages to scan for plugin
* manifests, then marks plugins that are actually used in the `plugins: [...]`
* array as requiredByTemplate.
*/
function runPluginsSync(options) {
	const cwd = process.cwd();
	const outputPath = path.resolve(cwd, options.output || "appkit.plugins.json");
	if (!isWithinDirectory(outputPath, cwd)) {
		console.error(`Error: Output path "${options.output}" resolves outside the project directory.`);
		process.exit(1);
	}
	if (!options.silent) console.log("Scanning for AppKit plugins...\n");
	const serverFile = findServerFile(cwd);
	let serverImports = [];
	let pluginUsages = /* @__PURE__ */ new Set();
	if (serverFile) {
		if (!options.silent) {
			const relativePath = path.relative(cwd, serverFile);
			console.log(`Server entry file: ${relativePath}`);
		}
		const content = fs.readFileSync(serverFile, "utf-8");
		const root = parse(serverFile.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript, content).root();
		serverImports = parseImports(root);
		pluginUsages = parsePluginUsages(root);
	} else if (!options.silent) console.log("No server entry file found. Checked:", SERVER_FILE_CANDIDATES.join(", "));
	const npmImports = serverImports.filter((i) => !i.source.startsWith(".") && !i.source.startsWith("/"));
	const localImports = serverImports.filter((i) => i.source.startsWith(".") || i.source.startsWith("/"));
	const plugins = {};
	if (options.pluginsDir) {
		const resolvedDir = path.resolve(cwd, options.pluginsDir);
		const pkgName = options.packageName ?? "@databricks/appkit";
		if (!options.silent) console.log(`Scanning plugins directory: ${options.pluginsDir}`);
		Object.assign(plugins, scanPluginsDir(resolvedDir, pkgName));
	} else {
		const npmPackages = new Set([...KNOWN_PLUGIN_PACKAGES, ...npmImports.map((i) => i.source)]);
		Object.assign(plugins, scanForPlugins(cwd, npmPackages));
	}
	if (serverFile && localImports.length > 0) {
		const localPlugins = discoverLocalPlugins(localImports, path.dirname(serverFile), cwd);
		Object.assign(plugins, localPlugins);
	}
	const pluginCount = Object.keys(plugins).length;
	if (pluginCount === 0) {
		if (options.silent) {
			writeManifest(outputPath, { plugins: {} }, options);
			return;
		}
		console.log("No plugins found.");
		if (options.pluginsDir) console.log(`\nNo manifest.json files found in: ${options.pluginsDir}`);
		else console.log("\nMake sure you have plugin packages installed.");
		process.exit(1);
	}
	const serverFileDir = serverFile ? path.dirname(serverFile) : cwd;
	for (const imp of serverImports) {
		if (!pluginUsages.has(imp.name)) continue;
		const isLocal = imp.source.startsWith(".") || imp.source.startsWith("/");
		let plugin;
		if (isLocal) {
			const resolvedImportDir = path.resolve(serverFileDir, imp.source);
			plugin = Object.values(plugins).find((p) => {
				if (!p.package.startsWith(".")) return false;
				return path.resolve(cwd, p.package) === resolvedImportDir && p.name === imp.originalName;
			});
		} else plugin = Object.values(plugins).find((p) => p.package === imp.source && p.name === imp.originalName);
		if (plugin) plugin.requiredByTemplate = true;
	}
	if (options.requirePlugins) {
		const explicitNames = options.requirePlugins.split(",").map((s) => s.trim()).filter(Boolean);
		for (const name of explicitNames) if (plugins[name]) plugins[name].requiredByTemplate = true;
		else if (!options.silent) console.warn(`Warning: --require-plugins referenced "${name}" but no such plugin was discovered`);
	}
	if (!options.silent) {
		console.log(`\nFound ${pluginCount} plugin(s):`);
		for (const [name, manifest] of Object.entries(plugins)) {
			const resourceCount = manifest.resources.required.length + manifest.resources.optional.length;
			const resourceInfo = resourceCount > 0 ? ` [${resourceCount} resource(s)]` : "";
			const mandatoryTag = manifest.requiredByTemplate ? " (mandatory)" : "";
			console.log(`  ${manifest.requiredByTemplate ? "●" : "○"} ${manifest.displayName} (${name}) from ${manifest.package}${resourceInfo}${mandatoryTag}`);
		}
	}
	writeManifest(outputPath, { plugins }, options);
}
const pluginsSyncCommand = new Command("sync").description("Sync plugin manifests from installed packages into appkit.plugins.json").option("-w, --write", "Write the manifest file").option("-o, --output <path>", "Output file path (default: ./appkit.plugins.json)").option("-s, --silent", "Suppress output and never exit with error (for use in predev/prebuild hooks)").option("--require-plugins <names>", "Comma-separated plugin names to mark as requiredByTemplate (e.g. server,analytics)").option("--plugins-dir <path>", "Scan this directory for plugin subdirectories with manifest.json (instead of node_modules)").option("--package-name <name>", "Package name to assign to plugins found via --plugins-dir (default: @databricks/appkit)").action(runPluginsSync);

//#endregion
export { pluginsSyncCommand };
//# sourceMappingURL=sync.js.map