import { existsSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join, relative, resolve } from "node:path";
import { getAgentDir, SettingsManager } from "@singularity-forge/coding-agent";
import { PluginImporter } from "./plugin-importer.js";

const SKIP_DIRS = new Set([
	".git",
	"node_modules",
	".worktrees",
	"dist",
	"build",
	".next",
	".turbo",
	"cache",
	".cache",
]);
function uniqueExistingDirs(paths) {
	const seen = new Set();
	const out = [];
	for (const candidate of paths) {
		const resolvedPath = resolve(candidate);
		if (seen.has(resolvedPath)) continue;
		seen.add(resolvedPath);
		if (existsSync(resolvedPath)) out.push(resolvedPath);
	}
	return out;
}
export function getClaudeSearchRoots(cwd) {
	const home = homedir();
	const parent = resolve(cwd, "..");
	const grandparent = resolve(cwd, "..", "..");
	// Claude Code user-scope skills live under ~/.claude/skills.
	// Keep sibling/local clone fallbacks for developer workflows, but they are
	// examples/convenience paths rather than the primary Claude storage model.
	const skillRoots = uniqueExistingDirs([
		join(home, ".claude", "skills"),
		join(home, "repos", "claude_skills"),
		join(home, "repos", "skills"),
		join(parent, "claude_skills"),
		join(parent, "skills"),
		join(grandparent, "claude_skills"),
		join(grandparent, "skills"),
	]);
	// Anthropic docs model marketplaces as sources users add with
	// `/plugin marketplace add ...`, and Claude stores those marketplaces under
	// ~/.claude/plugins/marketplaces/. Installed plugin payloads are copied into
	// ~/.claude/plugins/cache/. We prefer those stable Claude-managed locations
	// before local example clones.
	const pluginRoots = uniqueExistingDirs([
		join(home, ".claude", "plugins", "marketplaces"),
		join(home, ".claude", "plugins", "cache"),
		join(home, ".claude", "plugins"),
		join(home, "repos", "claude-plugins-official"),
		join(home, "repos", "claude_skills"),
		join(parent, "claude-plugins-official"),
		join(parent, "claude_skills"),
		join(grandparent, "claude-plugins-official"),
		join(grandparent, "claude_skills"),
	]);
	return { skillRoots, pluginRoots };
}
function sourceLabel(path) {
	const home = homedir();
	if (path.startsWith(join(home, ".claude"))) return "claude-home";
	if (path.startsWith(join(home, "repos"))) return "repos";
	return "local";
}
/**
 * Check if a path is a marketplace directory (contains .claude-plugin/marketplace.json).
 * Marketplace paths use the PluginImporter flow; non-marketplace use the legacy flat flow.
 */
function isMarketplacePath(pluginPath) {
	const marketplaceJson = join(
		pluginPath,
		".claude-plugin",
		"marketplace.json",
	);
	return existsSync(marketplaceJson);
}
/**
 * Detect which plugin roots are marketplaces and which are legacy flat paths.
 *
 * Claude Code stores marketplace sources under ~/.claude/plugins/marketplaces/.
 * Each subdirectory (e.g. marketplaces/confluent/) is a marketplace repo that
 * contains .claude-plugin/marketplace.json. The parent directory itself does not
 * have a marketplace.json, so we scan one level deeper when the root isn't
 * directly a marketplace.
 */
export function categorizePluginRoots(pluginRoots) {
	const marketplaces = [];
	const flat = [];
	const seen = new Set();
	for (const root of pluginRoots) {
		if (isMarketplacePath(root)) {
			if (!seen.has(root)) {
				marketplaces.push(root);
				seen.add(root);
			}
		} else {
			// The root itself isn't a marketplace — check if it's a container of
			// marketplaces (e.g. ~/.claude/plugins/marketplaces/ contains subdirs
			// like confluent/, claude-hud/, each with their own marketplace.json).
			let foundChild = false;
			try {
				const entries = readdirSync(root, { withFileTypes: true });
				for (const entry of entries) {
					if (!entry.isDirectory()) continue;
					if (SKIP_DIRS.has(entry.name)) continue;
					const childPath = join(root, entry.name);
					if (isMarketplacePath(childPath) && !seen.has(childPath)) {
						marketplaces.push(childPath);
						seen.add(childPath);
						foundChild = true;
					}
				}
			} catch {
				// Can't read directory — fall through to flat
			}
			if (!foundChild) {
				flat.push(root);
			}
		}
	}
	return { marketplaces, flat };
}
function walkDirs(root, visit, maxDepth = 4) {
	function walk(dir, depth) {
		visit(dir, depth);
		if (depth >= maxDepth) return;
		let entries = [];
		try {
			entries = readdirSync(dir, { withFileTypes: true });
		} catch {
			return;
		}
		for (const entry of entries) {
			if (!entry.isDirectory()) continue;
			if (SKIP_DIRS.has(entry.name)) continue;
			walk(join(dir, entry.name), depth + 1);
		}
	}
	walk(root, 0);
}
export function discoverClaudeSkills(cwd) {
	const { skillRoots } = getClaudeSearchRoots(cwd);
	const results = [];
	const seen = new Set();
	for (const root of skillRoots) {
		walkDirs(
			root,
			(dir) => {
				const skillFile = join(dir, "SKILL.md");
				if (!existsSync(skillFile)) return;
				const resolvedDir = resolve(dir);
				if (seen.has(resolvedDir)) return;
				seen.add(resolvedDir);
				results.push({
					type: "skill",
					name: basename(dir),
					path: resolvedDir,
					root,
					sourceLabel: sourceLabel(root),
				});
			},
			5,
		);
	}
	return results.sort(
		(a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path),
	);
}
export function discoverClaudePlugins(cwd) {
	const { pluginRoots } = getClaudeSearchRoots(cwd);
	const results = [];
	const seen = new Set();
	for (const root of pluginRoots) {
		walkDirs(
			root,
			(dir) => {
				// Recognize both npm-style plugins (package.json) and Claude Code plugins
				// (.claude-plugin/plugin.json). Claude marketplace-installed plugins use
				// the latter format exclusively.
				const pkgPath = join(dir, "package.json");
				const claudePluginPath = join(dir, ".claude-plugin", "plugin.json");
				const hasPkg = existsSync(pkgPath);
				const hasClaudePlugin = existsSync(claudePluginPath);
				if (!hasPkg && !hasClaudePlugin) return;
				const resolvedDir = resolve(dir);
				if (seen.has(resolvedDir)) return;
				seen.add(resolvedDir);
				let packageName;
				if (hasPkg) {
					try {
						const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
						packageName = pkg.name;
					} catch {
						packageName = undefined;
					}
				} else if (hasClaudePlugin) {
					try {
						const manifest = JSON.parse(readFileSync(claudePluginPath, "utf8"));
						packageName = manifest.name;
					} catch {
						packageName = undefined;
					}
				}
				results.push({
					type: "plugin",
					name: packageName || basename(dir),
					packageName,
					path: resolvedDir,
					root,
					sourceLabel: sourceLabel(root),
				});
			},
			4,
		);
	}
	return results.sort(
		(a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path),
	);
}
async function chooseMany(ctx, title, candidates) {
	if (candidates.length === 0) return [];
	const mode = await ctx.ui.select(`${title} (${candidates.length} found)`, [
		"Import all discovered",
		"Select individually",
		"Cancel",
	]);
	if (!mode || mode === "Cancel") return [];
	if (mode === "Import all discovered") return candidates;
	const remaining = [...candidates];
	const selected = [];
	while (remaining.length > 0) {
		const options = [
			...remaining.map(
				(item) =>
					`${item.name} — ${item.sourceLabel} — ${relative(item.root, item.path) || "."}`,
			),
			"Done selecting",
		];
		const picked = await ctx.ui.select(`${title}: choose an item`, options);
		if (!picked || picked === "Done selecting") break;
		const pickedStr = Array.isArray(picked) ? picked[0] : picked;
		if (!pickedStr) break;
		const idx = options.indexOf(pickedStr);
		if (idx < 0 || idx >= remaining.length) break;
		selected.push(remaining[idx]);
		remaining.splice(idx, 1);
	}
	return selected;
}
function mergeStringList(existing, additions) {
	const list = Array.isArray(existing)
		? existing.filter((v) => typeof v === "string")
		: [];
	const seen = new Set(list);
	for (const item of additions) {
		if (!seen.has(item)) {
			list.push(item);
			seen.add(item);
		}
	}
	return list;
}
function mergePackageSources(existing, additions) {
	const current = Array.isArray(existing)
		? existing.filter(
				(v) =>
					typeof v === "string" ||
					(typeof v === "object" && v !== null && typeof v.source === "string"),
			)
		: [];
	const seen = new Set(
		current.map((entry) => (typeof entry === "string" ? entry : entry.source)),
	);
	const merged = [...current];
	for (const add of additions) {
		if (!seen.has(add)) {
			merged.push(add);
			seen.add(add);
		}
	}
	return merged;
}
/**
 * Format a component for display in selection UI.
 */
function formatComponentForSelection(comp) {
	const typeLabel = comp.type === "skill" ? "🔧" : "🤖";
	const nsLabel = comp.namespace ? `${comp.namespace}:` : "";
	return `${typeLabel} ${nsLabel}${comp.name}`;
}
/**
 * Present marketplace components for user selection, grouped by plugin.
 * Returns the selected components for import.
 */
async function selectMarketplaceComponents(ctx, importer, scope) {
	const plugins = importer.getDiscoveredPlugins();
	if (plugins.length === 0) {
		ctx.ui.notify("No plugins discovered in marketplace.", "info");
		return [];
	}
	// Build component candidates grouped by plugin
	const allComponents = [];
	for (const plugin of plugins) {
		const components = importer.selectComponents(
			(c) => c.namespace === plugin.canonicalName,
		);
		for (const comp of components) {
			allComponents.push({
				component: comp,
				displayName: formatComponentForSelection(comp),
				pluginName: plugin.canonicalName,
			});
		}
	}
	if (allComponents.length === 0) {
		ctx.ui.notify(
			"No components (skills/agents) found in marketplace plugins.",
			"info",
		);
		return [];
	}
	// Ask user for selection mode
	const mode = await ctx.ui.select(
		`Marketplace components → ${scope} config (${allComponents.length} found across ${plugins.length} plugins)`,
		[
			"Import all components",
			"Select by plugin",
			"Select individually",
			"Cancel",
		],
	);
	if (!mode || mode === "Cancel") return [];
	if (mode === "Import all components") {
		return allComponents.map((c) => c.component);
	}
	if (mode === "Select by plugin") {
		// Let user select plugins, then import all their components
		const pluginNames = plugins.map((p) => p.canonicalName);
		const selectedPluginNames = [];
		while (true) {
			const remaining = pluginNames.filter(
				(n) => !selectedPluginNames.includes(n),
			);
			if (remaining.length === 0) break;
			const options = [...remaining, "Done selecting"];
			const picked = await ctx.ui.select(
				"Select a plugin to import all its components",
				options,
			);
			if (!picked || picked === "Done selecting") break;
			const pickedStr = Array.isArray(picked) ? picked[0] : picked;
			if (!pickedStr) break;
			selectedPluginNames.push(pickedStr);
		}
		return allComponents
			.filter((c) => selectedPluginNames.includes(c.pluginName))
			.map((c) => c.component);
	}
	// Select individually
	const remaining = [...allComponents];
	const selected = [];
	while (remaining.length > 0) {
		const options = remaining.map((c) => `${c.displayName} — ${c.pluginName}`);
		options.push("Done selecting");
		const picked = await ctx.ui.select("Select a component to import", options);
		if (!picked || picked === "Done selecting") break;
		const pickedStr = Array.isArray(picked) ? picked[0] : picked;
		if (!pickedStr) break;
		const idx = options.indexOf(pickedStr);
		if (idx < 0 || idx >= remaining.length) break;
		selected.push(remaining[idx].component);
		remaining.splice(idx, 1);
	}
	return selected;
}
/**
 * Format diagnostics for display to user.
 * Returns a human-readable summary string.
 */
function formatDiagnosticsForUser(diagnostics) {
	const lines = [];
	const errors = diagnostics.filter((d) => d.severity === "error");
	const warnings = diagnostics.filter((d) => d.severity === "warning");
	if (errors.length > 0) {
		lines.push(`❌ ${errors.length} error(s) blocking import:`);
		for (const err of errors) {
			lines.push(`   - ${err.class}: ${err.involvedCanonicalNames.join(", ")}`);
			lines.push(`     ${err.remediation}`);
		}
	}
	if (warnings.length > 0) {
		lines.push(`⚠️ ${warnings.length} warning(s):`);
		for (const warn of warnings) {
			lines.push(
				`   - ${warn.class}: ${warn.involvedCanonicalNames.join(", ")}`,
			);
		}
	}
	return lines.join("\n");
}
/**
 * Persist import manifest entries to settings.
 * Maps manifest entries to the appropriate settings format.
 */
function persistManifestToSettings(manifestEntries, settingsManager, scope) {
	// Group entries by namespace for organized persistence
	const skillPaths = manifestEntries
		.filter((e) => e.type === "skill")
		.map((e) => e.filePath);
	const _agentPaths = manifestEntries
		.filter((e) => e.type === "agent")
		.map((e) => e.filePath);
	// For marketplace plugins, we also want to store plugin-level metadata
	// Currently this adds component paths to skills/agents lists
	// Future enhancement: store canonical names with metadata
	if (skillPaths.length > 0) {
		if (scope === "project") {
			settingsManager.setProjectSkillPaths(
				mergeStringList(
					settingsManager.getProjectSettings().skills,
					skillPaths,
				),
			);
		} else {
			settingsManager.setSkillPaths(
				mergeStringList(settingsManager.getGlobalSettings().skills, skillPaths),
			);
		}
	}
	// Do not persist imported marketplace agents into settings.packages.
	// Claude plugin agent directories contain markdown agent definitions, not loadable Pi
	// extension packages. Writing `.../agents` paths into packages makes startup treat
	// them as extension roots and produces module-load errors.
	//
	// For now, marketplace agents remain discoverable via the import manifest and
	// canonical metadata, but are not persisted into package sources.
}
export async function runClaudeImportFlow(ctx, scope, readPrefs, writePrefs) {
	const cwd = process.cwd();
	const settingsManager = SettingsManager.create(cwd, getAgentDir());
	const { skillRoots: _skillRoots, pluginRoots } = getClaudeSearchRoots(cwd);
	// Categorize plugin roots into marketplaces vs flat paths
	const { marketplaces, flat } = categorizePluginRoots(pluginRoots);
	// Determine import mode
	const assetChoice = await ctx.ui.select(
		"Import Claude assets into SF/Pi config",
		["Skills + plugins", "Skills only", "Plugins only", "Cancel"],
	);
	if (!assetChoice || assetChoice === "Cancel") return;
	const importSkills = assetChoice !== "Plugins only";
	const importPlugins = assetChoice !== "Skills only";
	// Track what we're importing
	let importedSkillsCount = 0;
	let importedPluginsCount = 0;
	let importedMarketplaceComponents = 0;
	const canonicalNamesPersisted = [];
	// ========== SKILLS (legacy flat flow) ==========
	if (importSkills) {
		const discoveredSkills = discoverClaudeSkills(cwd);
		const selectedSkills = await chooseMany(
			ctx,
			`Claude skills → ${scope} preferences`,
			discoveredSkills,
		);
		if (selectedSkills.length > 0) {
			const prefMode = await ctx.ui.select(
				"How should SF treat the imported skills?",
				[
					"Always use when relevant",
					"Prefer when relevant",
					"Do not modify skill preferences",
				],
			);
			const prefs = readPrefs();
			const skillPaths = selectedSkills.map((skill) => skill.path);
			if (prefMode === "Always use when relevant") {
				prefs.always_use_skills = mergeStringList(
					prefs.always_use_skills,
					skillPaths,
				);
			} else if (prefMode === "Prefer when relevant") {
				prefs.prefer_skills = mergeStringList(prefs.prefer_skills, skillPaths);
			}
			await writePrefs(prefs);
			if (scope === "project") {
				settingsManager.setProjectSkillPaths(
					mergeStringList(
						settingsManager.getProjectSettings().skills,
						skillPaths,
					),
				);
			} else {
				settingsManager.setSkillPaths(
					mergeStringList(
						settingsManager.getGlobalSettings().skills,
						skillPaths,
					),
				);
			}
			importedSkillsCount = selectedSkills.length;
		}
	}
	// ========== MARKETPLACE PLUGINS (new PluginImporter flow) ==========
	if (importPlugins && marketplaces.length > 0) {
		const marketplaceChoice = await ctx.ui.select(
			`Found ${marketplaces.length} marketplace(s). Import from marketplace?`,
			[
				"Yes - discover plugins and select components",
				"Skip marketplaces (use legacy plugin paths only)",
				"Cancel",
			],
		);
		if (marketplaceChoice === "Yes - discover plugins and select components") {
			// Instantiate PluginImporter and discover
			const importer = new PluginImporter();
			const discovery = importer.discover(marketplaces);
			if (discovery.summary.totalPlugins > 0) {
				// Present components for selection
				const selectedComponents = await selectMarketplaceComponents(
					ctx,
					importer,
					scope,
				);
				if (selectedComponents.length > 0) {
					// Run validation (pre-import diagnostics)
					const validation = importer.validateImport(selectedComponents);
					// Show diagnostics
					if (validation.diagnostics.length > 0) {
						const diagMessage = formatDiagnosticsForUser(
							validation.diagnostics,
						);
						ctx.ui.notify(
							diagMessage,
							validation.canProceed ? "warning" : "error",
						);
						// Block if errors exist
						if (!validation.canProceed) {
							ctx.ui.notify(
								"Import blocked due to canonical name conflicts. Please resolve the errors above.",
								"error",
							);
							return;
						}
						// Warn but allow proceed for warnings
						const proceed = await ctx.ui.select(
							"Warnings detected. Continue with import?",
							["Yes, continue", "Cancel"],
						);
						if (proceed !== "Yes, continue") {
							return;
						}
					}
					// Generate manifest and persist
					const manifest = importer.getImportManifest(selectedComponents);
					persistManifestToSettings(manifest.entries, settingsManager, scope);
					importedMarketplaceComponents = selectedComponents.length;
					canonicalNamesPersisted.push(
						...manifest.entries.map((e) => e.canonicalName),
					);
				}
			} else {
				ctx.ui.notify(
					`No plugins discovered in ${marketplaces.length} marketplace(s).`,
					"info",
				);
			}
		}
	}
	// ========== FLAT PLUGIN PATHS (legacy flow) ==========
	if (importPlugins && flat.length > 0) {
		// Use legacy discovery for non-marketplace paths
		const discoveredPlugins = [];
		const seen = new Set();
		for (const root of flat) {
			walkDirs(
				root,
				(dir) => {
					const pkgPath = join(dir, "package.json");
					if (!existsSync(pkgPath)) return;
					const resolvedDir = resolve(dir);
					if (seen.has(resolvedDir)) return;
					seen.add(resolvedDir);
					let packageName;
					try {
						const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
						packageName = pkg.name;
					} catch {
						packageName = undefined;
					}
					discoveredPlugins.push({
						type: "plugin",
						name: packageName || basename(dir),
						packageName,
						path: resolvedDir,
						root,
						sourceLabel: sourceLabel(root),
					});
				},
				4,
			);
		}
		const sortedPlugins = discoveredPlugins.sort(
			(a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path),
		);
		const selectedPlugins = await chooseMany(
			ctx,
			`Claude plugins/packages → ${scope} Pi settings`,
			sortedPlugins,
		);
		if (selectedPlugins.length > 0) {
			const pluginPaths = selectedPlugins.map((plugin) => plugin.path);
			if (scope === "project") {
				settingsManager.setProjectPackages(
					mergePackageSources(
						settingsManager.getProjectSettings().packages,
						pluginPaths,
					),
				);
			} else {
				settingsManager.setPackages(
					mergePackageSources(
						settingsManager.getGlobalSettings().packages,
						pluginPaths,
					),
				);
			}
			importedPluginsCount = selectedPlugins.length;
		}
	}
	// ========== FINAL SUMMARY ==========
	if (
		importedSkillsCount === 0 &&
		importedPluginsCount === 0 &&
		importedMarketplaceComponents === 0
	) {
		ctx.ui.notify("Claude import cancelled or nothing selected.", "info");
		return;
	}
	await ctx.waitForIdle();
	await ctx.reload();
	const lines = [
		`Imported Claude assets into ${scope} config:`,
		`- Skills (flat): ${importedSkillsCount}`,
		`- Plugins (flat paths): ${importedPluginsCount}`,
		`- Marketplace components: ${importedMarketplaceComponents}`,
	];
	if (importedSkillsCount > 0) {
		lines.push(
			`- Skill paths added to Pi settings (${scope}) for availability`,
		);
		lines.push(`- Skill refs added to SF preferences (${scope}) when selected`);
	}
	if (importedPluginsCount > 0) {
		lines.push(
			`- Plugin/package paths added to Pi settings (${scope}) packages`,
		);
	}
	if (importedMarketplaceComponents > 0) {
		lines.push(
			`- Canonical names preserved: ${canonicalNamesPersisted.length} entries`,
		);
		if (canonicalNamesPersisted.length <= 10) {
			lines.push(`  Names: ${canonicalNamesPersisted.join(", ")}`);
		}
	}
	ctx.ui.notify(lines.join("\n"), "info");
}
