/**
 * resource-display.ts — utility functions for formatting loaded-resource paths and diagnostics.
 *
 * Purpose: isolate the pure display-formatting logic that was previously embedded as private
 * methods in InteractiveMode. These functions have no side-effects and take all their inputs
 * as parameters so they can be unit-tested and reused without a class instance.
 *
 * Consumer: interactive-mode.ts (showLoadedResources, formatDiagnostics callsites).
 */

import * as os from "node:os";
import * as path from "node:path";
import type { ResourceDiagnostic } from "../../core/resource-loader.js";
import { theme } from "./theme/theme.js";

/** Replace the home directory prefix with `~` for compact display. */
export function formatDisplayPath(p: string): string {
	const home = os.homedir();
	let result = p;
	if (result.startsWith(home)) {
		result = `~${result.slice(home.length)}`;
	}
	return result;
}

/**
 * Get a short path relative to the package root for display.
 * npm packages show a path relative to node_modules/pkg/; git packages relative to repo root;
 * local paths fall back to formatDisplayPath.
 */
export function getShortPath(fullPath: string, source: string): string {
	const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
	if (npmMatch && source.startsWith("npm:")) {
		return npmMatch[2];
	}

	const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
	if (gitMatch && source.startsWith("git:")) {
		return gitMatch[1];
	}

	return formatDisplayPath(fullPath);
}

/** Map a resource source + scope to a human-readable label and accent color. */
export function getDisplaySourceInfo(
	source: string,
	scope: string,
): { label: string; scopeLabel?: string; color: "accent" | "muted" } {
	if (source === "local") {
		if (scope === "user") {
			return { label: "user", color: "muted" };
		}
		if (scope === "project") {
			return { label: "project", color: "muted" };
		}
		if (scope === "temporary") {
			return { label: "path", scopeLabel: "temp", color: "muted" };
		}
		return { label: "path", color: "muted" };
	}

	if (source === "cli") {
		return {
			label: "path",
			scopeLabel: scope === "temporary" ? "temp" : undefined,
			color: "muted",
		};
	}

	const scopeLabel =
		scope === "user"
			? "user"
			: scope === "project"
				? "project"
				: scope === "temporary"
					? "temp"
					: undefined;
	return { label: source, scopeLabel, color: "accent" };
}

/** Classify a resource source+scope into one of the three display groups. */
export function getScopeGroup(
	source: string,
	scope: string,
): "user" | "project" | "path" {
	if (source === "cli" || scope === "temporary") return "path";
	if (scope === "user") return "user";
	if (scope === "project") return "project";
	return "path";
}

/** True when the source is an npm or git package (i.e. managed by the package manager). */
export function isPackageSource(source: string): boolean {
	return source.startsWith("npm:") || source.startsWith("git:");
}

/**
 * Group a list of resource paths by their user/project/path scope bucket.
 * Within each bucket, package-manager sources are further grouped by package.
 */
export function buildScopeGroups(
	paths: string[],
	metadata: Map<string, { source: string; scope: string; origin: string }>,
): Array<{
	scope: "user" | "project" | "path";
	paths: string[];
	packages: Map<string, string[]>;
}> {
	const groups: Record<
		"user" | "project" | "path",
		{
			scope: "user" | "project" | "path";
			paths: string[];
			packages: Map<string, string[]>;
		}
	> = {
		user: { scope: "user", paths: [], packages: new Map() },
		project: { scope: "project", paths: [], packages: new Map() },
		path: { scope: "path", paths: [], packages: new Map() },
	};

	for (const p of paths) {
		const meta = findMetadata(p, metadata);
		const source = meta?.source ?? "local";
		const scope = meta?.scope ?? "project";
		const groupKey = getScopeGroup(source, scope);
		const group = groups[groupKey];

		if (isPackageSource(source)) {
			const list = group.packages.get(source) ?? [];
			list.push(p);
			group.packages.set(source, list);
		} else {
			group.paths.push(p);
		}
	}

	return [groups.project, groups.user, groups.path].filter(
		(group) => group.paths.length > 0 || group.packages.size > 0,
	);
}

/** Render scope groups into a multi-line indented string for display. */
export function formatScopeGroups(
	groups: Array<{
		scope: "user" | "project" | "path";
		paths: string[];
		packages: Map<string, string[]>;
	}>,
	options: {
		formatPath: (p: string) => string;
		formatPackagePath: (p: string, source: string) => string;
	},
): string {
	const lines: string[] = [];

	for (const group of groups) {
		lines.push(`  ${theme.fg("accent", group.scope)}`);

		const sortedPaths = [...group.paths].sort((a, b) => a.localeCompare(b));
		for (const p of sortedPaths) {
			lines.push(theme.fg("dim", `    ${options.formatPath(p)}`));
		}

		const sortedPackages = Array.from(group.packages.entries()).sort(
			([a], [b]) => a.localeCompare(b),
		);
		for (const [source, paths] of sortedPackages) {
			lines.push(`    ${theme.fg("mdLink", source)}`);
			const sortedPackagePaths = [...paths].sort((a, b) => a.localeCompare(b));
			for (const p of sortedPackagePaths) {
				lines.push(
					theme.fg("dim", `      ${options.formatPackagePath(p, source)}`),
				);
			}
		}
	}

	return lines.join("\n");
}

/**
 * Find metadata for a path, checking parent directories if exact match fails.
 * The package manager stores metadata for directories, but we display file paths.
 */
export function findMetadata(
	p: string,
	metadata: Map<string, { source: string; scope: string; origin: string }>,
): { source: string; scope: string; origin: string } | undefined {
	const exact = metadata.get(p);
	if (exact) return exact;

	let current = p;
	let parent = path.dirname(current);
	while (parent !== current) {
		const meta = metadata.get(parent);
		if (meta) return meta;
		current = parent;
		parent = path.dirname(current);
	}

	return undefined;
}

/** Format a path with its source/scope info derived from loader metadata. */
export function formatPathWithSource(
	p: string,
	metadata: Map<string, { source: string; scope: string; origin: string }>,
): string {
	const meta = findMetadata(p, metadata);
	if (meta) {
		const shortPath = getShortPath(p, meta.source);
		const { label, scopeLabel } = getDisplaySourceInfo(meta.source, meta.scope);
		const labelText = scopeLabel ? `${label} (${scopeLabel})` : label;
		return `${labelText} ${shortPath}`;
	}
	return formatDisplayPath(p);
}

/**
 * Format resource diagnostics with nice collision display using metadata.
 *
 * Purpose: render skill/prompt/extension load errors and name collisions into
 * a human-readable multi-line string so showLoadedResources can add them to chat.
 *
 * Consumer: interactive-mode.ts showLoadedResources.
 */
export function formatDiagnostics(
	diagnostics: readonly ResourceDiagnostic[],
	metadata: Map<string, { source: string; scope: string; origin: string }>,
): string {
	const lines: string[] = [];

	const collisions = new Map<string, ResourceDiagnostic[]>();
	const otherDiagnostics: ResourceDiagnostic[] = [];

	for (const d of diagnostics) {
		if (d.type === "collision" && d.collision) {
			const list = collisions.get(d.collision.name) ?? [];
			list.push(d);
			collisions.set(d.collision.name, list);
		} else {
			otherDiagnostics.push(d);
		}
	}

	for (const [name, collisionList] of collisions) {
		const first = collisionList[0]?.collision;
		if (!first) continue;
		lines.push(theme.fg("warning", `  "${name}" collision:`));
		lines.push(
			theme.fg(
				"dim",
				`    ${theme.fg("success", "✓")} ${formatPathWithSource(first.winnerPath, metadata)}`,
			),
		);
		for (const d of collisionList) {
			if (d.collision) {
				lines.push(
					theme.fg(
						"dim",
						`    ${theme.fg("warning", "✗")} ${formatPathWithSource(d.collision.loserPath, metadata)} (skipped)`,
					),
				);
			}
		}
	}

	for (const d of otherDiagnostics) {
		if (d.path) {
			const sourceInfo = formatPathWithSource(d.path, metadata);
			lines.push(
				theme.fg(d.type === "error" ? "error" : "warning", `  ${sourceInfo}`),
			);
			lines.push(
				theme.fg(d.type === "error" ? "error" : "warning", `    ${d.message}`),
			);
		} else {
			lines.push(
				theme.fg(d.type === "error" ? "error" : "warning", `  ${d.message}`),
			);
		}
	}

	return lines.join("\n");
}
