/**
 * Gap Audit — detect orphaned/unwired artifacts in the SF extension.
 *
 * Purpose: automatically find dead code, unreferenced prompts, undispatched
 * command handlers, and shipped-but-unimported native modules. Results are
 * written to self-feedback so they surface in SELF-FEEDBACK.md and can be triaged.
 *
 * Consumer: session_start drain hook in register-hooks.ts.
 */
import { createHash } from "node:crypto";
import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "node:fs";
import { join, relative } from "node:path";
import { recordSelfFeedback } from "./self-feedback.js";

const EXTENSION_SRC = import.meta.dirname;
const PROMPTS_DIR = join(EXTENSION_SRC, "prompts");
const COMMANDS_DIR = join(EXTENSION_SRC, "commands");
const HANDLERS_DIR = join(COMMANDS_DIR, "handlers");
const NATIVE_PKG = join(EXTENSION_SRC, "..", "..", "..", "native");
function hashFindings(findings) {
	const data = findings
		.map((f) => `${f.kind}:${f.name}:${f.path}`)
		.sort()
		.join("\n");
	return createHash("sha256").update(data).digest("hex").slice(0, 16);
}
function readFileLines(path) {
	try {
		return readFileSync(path, "utf-8").split("\n");
	} catch {
		return [];
	}
}
function grepImports(sourceDir, symbol) {
	try {
		const files = readdirSync(sourceDir, { recursive: true });
		for (const file of files) {
			if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
			const content = readFileSync(join(sourceDir, file), "utf-8");
			if (content.includes(symbol)) return true;
		}
	} catch {
		/* ignore */
	}
	return false;
}
/**
 * Known prompts loaded dynamically via variable/template names rather than
 * literal loadPrompt("name") calls. These are loaded through wrappers in
 * auto-prompts.ts, workflow-dispatch.ts, and other dispatchers.
 */
const DYNAMICALLY_LOADED_PROMPTS = new Set([
	"research-slice",
	"plan-slice",
	"execute-task",
	"workflow-start",
	"triage-self-feedback",
	"heal-skill",
	"product-audit",
	"refine-slice",
	"review-migration",
]);
function findOrphanPrompts() {
	const findings = [];
	try {
		const files = readdirSync(PROMPTS_DIR).filter((f) => f.endsWith(".md"));
		for (const file of files) {
			const name = file.slice(0, -3);
			// Skip templates that are loaded by convention (guided-* variants)
			if (name.startsWith("guided-")) continue;
			// Skip prompts known to be loaded dynamically through wrappers
			if (DYNAMICALLY_LOADED_PROMPTS.has(name)) continue;
			const loaded =
				grepImports(EXTENSION_SRC, `loadPrompt("${name}"`) ||
				grepImports(EXTENSION_SRC, `loadPrompt('${name}'`) ||
				grepImports(EXTENSION_SRC, `loadPrompt("${name}",`) ||
				grepImports(EXTENSION_SRC, `loadPrompt('${name}',`) ||
				// Some prompts are loaded by direct filesystem read (skill-health.ts,
				// migrate/command.ts, product-audit-tool.ts) rather than via loadPrompt.
				// Check for the bare filename literal as a strong indicator of intent.
				grepImports(EXTENSION_SRC, `"${name}.md"`) ||
				grepImports(EXTENSION_SRC, `'${name}.md'`);
			if (!loaded) {
				findings.push({
					kind: "orphan-prompt",
					name,
					path: relative(EXTENSION_SRC, join(PROMPTS_DIR, file)),
					detail: `Prompt "${name}" exists but no loadPrompt("${name}") call found in extension source`,
				});
			}
		}
	} catch {
		/* prompts dir may not exist in test env */
	}
	return findings;
}
function findOrphanHandlers() {
	const findings = [];
	try {
		const files = readdirSync(HANDLERS_DIR).filter((f) => f.endsWith(".ts"));
		for (const file of files) {
			const path = join(HANDLERS_DIR, file);
			const lines = readFileLines(path);
			for (let i = 0; i < lines.length; i++) {
				const line = lines[i];
				// Look for exported handle* functions
				const match = line.match(
					/export\s+(?:async\s+)?function\s+(handle\w+)/,
				);
				if (!match) continue;
				const handlerName = match[1];
				// Check if dispatched from ops.ts, workflow.ts, core.ts, auto.ts
				const dispatched = grepImports(COMMANDS_DIR, handlerName);
				if (!dispatched) {
					findings.push({
						kind: "orphan-handler",
						name: handlerName,
						path: relative(EXTENSION_SRC, path),
						detail: `${handlerName} exported from ${file} but never dispatched from commands/handlers/*.ts`,
					});
				}
			}
		}
	} catch {
		/* handlers dir may not exist */
	}
	return findings;
}
function findOrphanNative() {
	const findings = [];
	const nativeEditDir = join(NATIVE_PKG, "src", "edit");
	try {
		if (!existsSync(nativeEditDir)) return findings;
		const indexPath = join(nativeEditDir, "index.ts");
		if (!existsSync(indexPath)) return findings;
		const lines = readFileLines(indexPath);
		for (const line of lines) {
			const match = line.match(/export\s+(?:async\s+)?function\s+(\w+)/);
			if (!match) continue;
			const symbol = match[1];
			const imported = grepImports(EXTENSION_SRC, symbol);
			if (!imported) {
				findings.push({
					kind: "orphan-native",
					name: symbol,
					path: relative(EXTENSION_SRC, indexPath),
					detail: `Native edit function ${symbol} exported but never imported from SF extension`,
				});
			}
		}
	} catch {
		/* native pkg may not exist */
	}
	return findings;
}
function findOrphanCommands() {
	const findings = [];
	const catalogPath = join(COMMANDS_DIR, "catalog.ts");
	if (!existsSync(catalogPath)) return findings;
	const catalogLines = readFileLines(catalogPath);
	const advertisedCommands = [];
	for (const line of catalogLines) {
		// Match { cmd: "rate", desc: "..." } patterns
		const match = line.match(/cmd:\s*["'](\w+)["']/);
		if (match) advertisedCommands.push(match[1]);
	}
	// Check which are dispatched from ops.ts / workflow.ts / core.ts
	const dispatchFiles = ["ops.ts", "workflow.ts", "core.ts", "auto.ts"]
		.map((f) => join(HANDLERS_DIR, f))
		.filter(existsSync);
	for (const cmd of advertisedCommands) {
		let dispatched = false;
		for (const path of dispatchFiles) {
			const content = readFileSync(path, "utf-8");
			// Detect exact match: trimmed === "cmd" or trimmed === `cmd`
			if (content.includes(`"${cmd}"`) || content.includes(`'${cmd}'`)) {
				dispatched = true;
				break;
			}
			// Detect prefix match: startsWith("cmd ") or startsWith('cmd ')
			if (content.includes(`"${cmd} "`) || content.includes(`'${cmd} '`)) {
				dispatched = true;
				break;
			}
			// Detect grouped/aliased match: includes("cmd") in command arrays or switch cases
			// Look for the command in switch/case patterns: case "cmd": or case 'cmd':
			if (new RegExp(`cases+["']${cmd}["']`).test(content)) {
				dispatched = true;
				break;
			}
		}
		if (!dispatched) {
			findings.push({
				kind: "orphan-command",
				name: cmd,
				path: relative(EXTENSION_SRC, catalogPath),
				detail: `/${cmd} advertised in catalog but no dispatch branch found in handlers`,
			});
		}
	}
	return findings;
}
/**
 * Run the gap audit and file self-feedback entries for any findings.
 * Deduped by content hash so repeat runs don't multiply entries.
 *
 * @returns number of new findings filed (0 if all were already reported)
 */
export function runGapAudit(basePath = process.cwd()) {
	const findings = [
		...findOrphanPrompts(),
		...findOrphanHandlers(),
		...findOrphanNative(),
		...findOrphanCommands(),
	];
	if (findings.length === 0) return 0;
	const hash = hashFindings(findings);
	const hashPath = join(basePath, ".sf", "runtime", ".gap-audit-hash");
	// Check if we've already reported this exact set
	try {
		if (existsSync(hashPath)) {
			const prior = readFileSync(hashPath, "utf-8").trim();
			if (prior === hash) return 0;
		}
	} catch {
		/* ignore */
	}
	// File one self-feedback entry per finding kind, grouped
	const byKind = new Map();
	for (const f of findings) {
		const list = byKind.get(f.kind) ?? [];
		list.push(f);
		byKind.set(f.kind, list);
	}
	let filed = 0;
	for (const [kind, items] of byKind) {
		const severity = kind === "orphan-native" ? "high" : "medium";
		const summary = items.map((i) => i.name).join(", ");
		const evidence = items.map((i) => `- ${i.name}: ${i.detail}`).join("\n");
		const result = recordSelfFeedback(
			{
				kind: `gap-audit-${kind}`,
				severity: severity,
				summary: `${kind.replace("-", " ")}: ${summary}`,
				evidence,
				suggestedFix:
					kind === "orphan-prompt"
						? "Remove unused prompt or wire it into a loadPrompt call"
						: kind === "orphan-handler"
							? "Add dispatch branch in ops.ts/workflow.ts or remove dead export"
							: kind === "orphan-native"
								? "Wire native function into SF extension or remove from native package"
								: "Add dispatch branch for advertised command or remove from catalog",
				source: "agent",
			},
			basePath,
		);
		if (result) filed++;
	}
	// Write hash to prevent re-filing
	try {
		mkdirSync(join(basePath, ".sf", "runtime"), { recursive: true });
		writeFileSync(hashPath, hash, "utf-8");
	} catch {
		/* non-fatal */
	}
	return filed;
}
