/**
 * Milestone framing check — pre-flight sanity check run before milestone work begins.
 *
 * Reviews:
 *   1. Milestone CONTEXT.md / title against PROJECT.md vision
 *   2. .sf/NON-GOALS.md — does this milestone violate any?
 *   3. Category error: is this solving the right problem?
 *
 * Non-blocking: findings are surfaced as structured annotations.
 * The agent reads them, considers, and proceeds (or stops on severity=block).
 */
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { sfRoot } from "./paths.js";
/**
 * Check milestone framing against project vision, non-goals, and category-error heuristics.
 *
 * Reads:
 *   - <basePath>/PROJECT.md (vision)
 *   - <basePath>/.sf/NON-GOALS.md
 *   - <basePath>/.sf/milestones/<milestoneId>/<milestoneId>-CONTEXT.md (or CONTEXT.md)
 *
 * @param basePath - project root (cwd)
 * @param milestoneId - milestone ID string (e.g. "M001")
 * @returns array of findings, empty when nothing notable
 */
export function checkMilestoneFraming(basePath, milestoneId) {
	const findings = [];
	// ── Load inputs ──────────────────────────────────────────────────────────
	const projectMdPath = join(basePath, "PROJECT.md");
	const projectMd = safeRead(projectMdPath);
	const sfDir = sfRoot(basePath);
	const nonGoalsPath = join(sfDir, "NON-GOALS.md");
	const nonGoalsMd = safeRead(nonGoalsPath);
	// Try to find milestone context file
	const milestonePath = join(sfDir, "milestones", milestoneId);
	const contextCandidates = [
		join(milestonePath, `${milestoneId}-CONTEXT.md`),
		join(milestonePath, "CONTEXT.md"),
	];
	let contextMd = "";
	for (const candidate of contextCandidates) {
		const content = safeRead(candidate);
		if (content) {
			contextMd = content;
			break;
		}
	}
	if (!contextMd) return findings; // nothing to check
	const contextLower = contextMd.toLowerCase();
	// ── Anti-goal keyword check ──────────────────────────────────────────────
	if (nonGoalsMd) {
		const nonGoalLines = extractBulletLines(nonGoalsMd);
		for (const line of nonGoalLines) {
			const keywords = extractKeywords(line);
			const matched = keywords.filter((kw) => contextLower.includes(kw));
			if (matched.length > 0) {
				findings.push({
					concern: `Milestone description contains "${matched[0]}" — NON-GOALS.md entry: "${line.slice(0, 120)}"`,
					source: "anti_goal",
					severity: "warning",
				});
			}
		}
	}
	// ── Project vision alignment check ───────────────────────────────────────
	if (projectMd) {
		const visionKeywords = extractKeywords(projectMd.slice(0, 2000));
		const overlap = visionKeywords.filter((kw) => contextLower.includes(kw));
		if (overlap.length === 0) {
			findings.push({
				concern: `Milestone context has no clear overlap with PROJECT.md vision keywords — verify this milestone advances the stated project goals.`,
				source: "project_vision",
				severity: "info",
			});
		}
	}
	// ── Completed milestone overlap check ────────────────────────────────────
	try {
		const milestoneIds = listMilestoneDirs(sfDir).filter(
			(e) => /^M\d+/.test(e) && e !== milestoneId,
		);
		const contextTitle = extractTitle(contextMd);
		for (const otherId of milestoneIds) {
			const otherContextCandidates = [
				join(sfDir, "milestones", otherId, `${otherId}-CONTEXT.md`),
				join(sfDir, "milestones", otherId, "CONTEXT.md"),
			];
			for (const candidate of otherContextCandidates) {
				const otherContext = safeRead(candidate);
				if (otherContext) {
					const otherTitle = extractTitle(otherContext);
					if (
						contextTitle &&
						otherTitle &&
						titlesOverlap(contextTitle, otherTitle)
					) {
						findings.push({
							concern: `Milestone title "${contextTitle}" is similar to existing milestone ${otherId} "${otherTitle}" — verify this is not duplicate or already-completed work.`,
							source: "category_error",
							severity: "warning",
						});
					}
					break;
				}
			}
		}
	} catch {
		// Non-fatal; skip overlap check if directory enumeration fails
	}
	return findings;
}
/**
 * Format framing findings into a system-prompt block.
 * Returns empty string when there are no findings.
 */
export function formatFramingFindings(milestoneId, findings) {
	if (findings.length === 0) return "";
	const lines = findings.map((f) => {
		const label = f.severity.toUpperCase();
		return `- ${label}: ${f.concern}`;
	});
	return `\n\n[MILESTONE FRAMING CHECK — ${milestoneId}]\n${lines.join("\n")}`;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function safeRead(filePath) {
	if (!existsSync(filePath)) return "";
	try {
		return readFileSync(filePath, "utf-8").trim();
	} catch {
		return "";
	}
}
/**
 * Extract non-empty bullet lines (starting with - or *) from markdown text.
 */
function extractBulletLines(text) {
	return text
		.split("\n")
		.map((l) => l.trim())
		.filter((l) => l.startsWith("- ") || l.startsWith("* "))
		.map((l) => l.slice(2).trim())
		.filter((l) => l.length > 5);
}
/**
 * Extract significant lowercase keywords (length >= 5, non-common) from text.
 * Used for fuzzy matching between milestone context and non-goals / vision.
 */
function extractKeywords(text) {
	const stopwords = new Set([
		"the",
		"this",
		"that",
		"with",
		"from",
		"have",
		"when",
		"what",
		"will",
		"been",
		"each",
		"they",
		"them",
		"some",
		"also",
		"into",
		"more",
		"only",
		"over",
		"such",
		"than",
		"then",
		"there",
		"these",
		"those",
		"about",
		"after",
		"before",
		"where",
		"which",
		"while",
		"should",
		"would",
		"could",
		"because",
	]);
	return [
		...new Set(
			text
				.toLowerCase()
				.replace(/[^a-z0-9\s-]/g, " ")
				.split(/\s+/)
				.filter((w) => w.length >= 5 && !stopwords.has(w)),
		),
	];
}
function extractTitle(text) {
	const match = text.match(/^#\s+(.+)$/m);
	return match ? match[1].trim() : null;
}
function titlesOverlap(a, b) {
	const wordsA = new Set(
		a
			.toLowerCase()
			.split(/\s+/)
			.filter((w) => w.length >= 5),
	);
	const wordsB = b
		.toLowerCase()
		.split(/\s+/)
		.filter((w) => w.length >= 5);
	const shared = wordsB.filter((w) => wordsA.has(w));
	return shared.length >= 2;
}
/**
 * List milestone subdirectory names under <sfDir>/milestones/.
 * Returns empty array when the directory does not exist or cannot be read.
 */
function listMilestoneDirs(sfDir) {
	const milestonesDir = join(sfDir, "milestones");
	if (!existsSync(milestonesDir)) return [];
	try {
		return readdirSync(milestonesDir, { withFileTypes: false });
	} catch {
		return [];
	}
}
