/**
 * Doc-sync drift detection for tracked architecture documentation.
 *
 * Purpose: detect when ARCHITECTURE.md (and future CONVENTIONS.md/STACK.md)
 * are out of sync with the actual codebase structure. Returns a proposal
 * describing what changed, so the user can decide whether to update docs.
 *
 * Consumer: auto-post-unit-staging.ts (called after code-mutating phases).
 */

/** Tracked documentation files to check for drift. */
const TRACKED_DOCS = ["ARCHITECTURE.md", "CONVENTIONS.md", "STACK.md"];

/**
 * Parse the codemap table from ARCHITECTURE.md content.
 * Returns an array of { path, purpose } entries.
 *
 * Purpose: extract structured path data from Markdown tables for comparison.
 * Consumer: detectDocDrift().
 */
export function parseCodemapTable(content) {
	const lines = content.split("\n");
	const entries = [];
	let inTable = false;

	for (const line of lines) {
		if (line.trim().startsWith("|") && line.includes("`")) {
			inTable = true;
			// Skip header separator rows
			if (line.match(/^\|[\s\-|]+\|$/)) continue;

			const cells = line
				.split("|")
				.map((c) => c.trim())
				.filter(Boolean);
			if (cells.length >= 2 && cells[0].includes("`")) {
				const pathMatch = cells[0].match(/`([^`]+)`/);
				if (pathMatch) {
					entries.push({
						path: pathMatch[1],
						purpose: cells.slice(1).join(" | ").trim(),
					});
				}
			}
		} else if (inTable && !line.trim().startsWith("|")) {
			// End of table
			break;
		}
	}

	return entries;
}

/**
 * Check whether a filesystem path pattern matches an actual file.
 * Handles globs like `src/resources/extensions/sf/` (directory)
 * and `src/loader.ts` (file).
 *
 * Purpose: validate codemap entries against actual filesystem.
 * Consumer: detectDocDrift().
 */
function pathExistsOnDisk(docPath, existingPaths) {
	// Exact match
	if (existingPaths.has(docPath)) return true;
	// Directory prefix match (codemap uses trailing /)
	if (docPath.endsWith("/")) {
		for (const p of existingPaths) {
			if (p.startsWith(docPath) || p === docPath.slice(0, -1)) return true;
		}
	}
	// Prefix match (codemap entry is parent of actual files)
	for (const p of existingPaths) {
		if (p.startsWith(docPath.replace(/\/$/, "/"))) return true;
	}
	return false;
}

/**
 * Detect drift between tracked documentation and actual codebase state.
 *
 * Purpose: the core detection function — compares doc content against
 * filesystem reality and changed files to determine if docs are stale.
 * Consumer: getDocSyncProposal().
 *
 * @param {string} docContent - Content of the documentation file
 * @param {string[]} changedFiles - Files changed in the current unit
 * @param {Set<string>} existingPaths - Set of paths that exist on disk
 * @returns {Array<{type: string, detail: string}>|null} Drift items or null
 */
export function detectDocDrift(docContent, changedFiles, existingPaths) {
	if (!docContent || changedFiles.length === 0) return null;

	const entries = parseCodemapTable(docContent);
	if (entries.length === 0) return null;

	const drift = [];

	// Check 1: codemap paths that no longer exist
	for (const entry of entries) {
		// Skip non-local paths (e.g. ~/.sf/...)
		if (entry.path.startsWith("~")) continue;
		if (!pathExistsOnDisk(entry.path, existingPaths)) {
			drift.push({
				type: "stale_path",
				detail: `Codemap path \`${entry.path}\` no longer exists on disk`,
			});
		}
	}

	// Check 2: changed files that belong to undocumented directories
	const documentedDirs = new Set(
		entries
			.map((e) => e.path.replace(/\/[^/]*$/, "/"))
			.filter((p) => !p.startsWith("~")),
	);

	const undocumentedChanges = changedFiles.filter((f) => {
		// Check if any documented directory is a prefix of this file
		for (const dir of documentedDirs) {
			if (f.startsWith(dir)) return false;
		}
		// Skip .sf/ files — those are workspace, not codebase
		if (f.startsWith(".sf/")) return false;
		// Skip dist/ — it's generated
		if (f.startsWith("dist/")) return false;
		// Skip test-only files
		if (f.includes(".test.") && !documentedDirs.has("harness/")) return false;
		return true;
	});

	if (undocumentedChanges.length > 0) {
		// Extract unique directory prefixes
		const dirs = new Set(
			undocumentedChanges.map((f) => {
				const parts = f.split("/");
				return parts.length > 1 ? parts.slice(0, -1).join("/") + "/" : f;
			}),
		);
		for (const dir of dirs) {
			drift.push({
				type: "undocumented_path",
				detail: `Changed files under \`${dir}\` but this path is not in the codemap`,
			});
		}
	}

	return drift.length > 0 ? drift : null;
}

/**
 * Get a doc-sync proposal for the current unit's changes.
 * Reads tracked docs, runs drift detection, returns proposal or null.
 *
 * Purpose: orchestrate the drift check — read docs, gather filesystem state,
 * run detection. Returns a human-readable proposal.
 * Consumer: auto-post-unit-staging.ts.
 *
 * @param {string[]} changedFiles - Files changed in the current unit
 * @param {{ readFile: (path: string) => string|null }} io - IO abstraction for testing
 * @param {{ glob: (pattern: string) => string[] }} fs - Filesystem abstraction
 * @returns {{ doc: string, drift: Array }|null} Proposal or null
 */
export function getDocSyncProposal(changedFiles, io, fs) {
	if (!changedFiles || changedFiles.length === 0) return null;

	// Only check code-mutating changes (skip .sf/, dist/, docs-only)
	const codeChanges = changedFiles.filter(
		(f) =>
			!f.startsWith(".sf/") &&
			!f.startsWith("dist/") &&
			!f.startsWith("docs/") &&
			!f.startsWith("BUILD_PLAN"),
	);
	if (codeChanges.length === 0) return null;

	const proposals = [];

	for (const docFile of TRACKED_DOCS) {
		const content = io.readFile(docFile);
		if (!content) continue; // Doc doesn't exist — skip

		const existingPaths = new Set(fs.glob("**/*"));
		const drift = detectDocDrift(content, codeChanges, existingPaths);

		if (drift && drift.length > 0) {
			proposals.push({ doc: docFile, drift });
		}
	}

	return proposals.length > 0 ? proposals : null;
}

/**
 * Format a doc-sync proposal as a human-readable notification.
 *
 * Purpose: convert the machine-readable proposal into a user-facing message.
 * Consumer: auto-post-unit-staging.ts notification logging.
 */
export function formatDocSyncProposal(proposals) {
	if (!proposals || proposals.length === 0) return null;

	const lines = ["📄 Doc-sync: documentation may be out of date"];
	for (const { doc, drift } of proposals) {
		lines.push(`  ${doc}:`);
		for (const item of drift) {
			lines.push(`    - ${item.detail}`);
		}
	}
	lines.push("  Run `sf plan promote` to update if needed.");

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