// SF Extension — Record Promoter
// Scans docs/records/*.md for frontmatter-tagged actionable records and
// promotes each one to a .sf/milestones/M<NNN>/ structure.
// Called from ensureAgenticDocsScaffold (ADR-021 Phase C hook) and from
// the post-milestone-completion path in auto-post-unit.ts / auto.ts:stopAuto.
//
// Silent contract: no stdout/stderr in the no-op path. Failure is non-fatal;
// callers must wrap in try/catch. logWarning("scaffold", ...) only on errors.
import {
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from "node:fs";
import { basename, join } from "node:path";
import { addBacklogItem, isDbAvailable } from "./sf-db.js";
import { logWarning } from "./workflow-logger.js";
// ─── Frontmatter Parser ──────────────────────────────────────────────────────
/**
 * Parse the YAML frontmatter from a markdown file.
 * Returns null if the file has no frontmatter block (--- delimiters).
 * Handles simple scalar YAML only — no nested objects or multi-line values.
 */
export function parseRecordFrontmatter(filePath) {
	let content;
	try {
		content = readFileSync(filePath, "utf-8");
	} catch {
		return null;
	}
	// Frontmatter must start at the very beginning of the file
	if (!content.startsWith("---")) return null;
	const end = content.indexOf("\n---", 3);
	if (end === -1) return null;
	const block = content.slice(3, end).trim();
	const result = {};
	for (const line of block.split("\n")) {
		const colon = line.indexOf(":");
		if (colon === -1) continue;
		const key = line.slice(0, colon).trim();
		const raw = line.slice(colon + 1).trim();
		// Strip inline comments and optional quotes
		const val = raw
			.replace(/#.*$/, "")
			.trim()
			.replace(/^["']|["']$/g, "");
		if (key === "actionable") {
			result.actionable = val === "true";
		} else if (key === "promoted") {
			result.promoted = val === "true";
		} else if (key === "kind") {
			result.kind = val || undefined;
		} else if (key === "promoted_to") {
			result.promoted_to = val || undefined;
		} else if (key === "date") {
			result.date = val || undefined;
		}
	}
	return result;
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
/** Convert a filename or title string to a URL-safe slug. */
function toSlug(s) {
	return s
		.toLowerCase()
		.replace(/[^a-z0-9]+/g, "-")
		.replace(/^-+|-+$/g, "")
		.slice(0, 40);
}
/**
 * Derive the next milestone ID by scanning existing .sf/milestones/M* dirs.
 * Returns "M001" if no milestones exist yet.
 */
function nextMilestoneId(milestonesPath) {
	let max = 0;
	if (existsSync(milestonesPath)) {
		try {
			for (const entry of readdirSync(milestonesPath, {
				withFileTypes: true,
			})) {
				if (!entry.isDirectory()) continue;
				const match = entry.name.match(/^M(\d{3})/);
				if (match) {
					const n = parseInt(match[1], 10);
					if (n > max) max = n;
				}
			}
		} catch {
			/* non-fatal */
		}
	}
	return `M${String(max + 1).padStart(3, "0")}`;
}
/**
 * Extract H2 headings from markdown body content (after stripping frontmatter).
 * Returns the heading text for each `## Heading` line found.
 */
function extractH2Headings(body) {
	const headings = [];
	for (const line of body.split("\n")) {
		const m = line.match(/^##\s+(.+)$/);
		if (m) headings.push(m[1].trim());
	}
	return headings;
}
/** Strip YAML frontmatter block from file content. */
function stripFrontmatter(content) {
	if (!content.startsWith("---")) return content;
	const end = content.indexOf("\n---", 3);
	if (end === -1) return content;
	return content.slice(end + 4).trimStart();
}
/**
 * Derive a milestone title from a record filename.
 * "2026-05-02-bug-hunt-findings.md" → "Bug Hunt Findings"
 */
function titleFromFilename(filename) {
	return basename(filename, ".md")
		.replace(/^\d{4}-\d{2}-\d{2}-/, "")
		.split("-")
		.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
		.join(" ");
}
/** Update the frontmatter of a record file to mark it as promoted. */
function stampRecordPromoted(filePath, milestoneId) {
	const content = readFileSync(filePath, "utf-8");
	if (!content.startsWith("---")) {
		// No frontmatter at all — prepend minimal block
		const newFm = `---\npromoted: true\npromoted_to: ${milestoneId}\n---\n\n`;
		writeFileSync(filePath, newFm + content, "utf-8");
		return;
	}
	const end = content.indexOf("\n---", 3);
	if (end === -1) return; // malformed — leave alone
	const fmBlock = content.slice(3, end);
	let newBlock = fmBlock;
	// Upsert promoted and promoted_to keys
	if (/^promoted:/m.test(newBlock)) {
		newBlock = newBlock.replace(/^promoted:.*$/m, "promoted: true");
	} else {
		newBlock += "\npromoted: true";
	}
	if (/^promoted_to:/m.test(newBlock)) {
		newBlock = newBlock.replace(
			/^promoted_to:.*$/m,
			`promoted_to: ${milestoneId}`,
		);
	} else {
		newBlock += `\npromoted_to: ${milestoneId}`;
	}
	const after = content.slice(end); // starts with "\n---"
	writeFileSync(filePath, `---${newBlock}${after}`, "utf-8");
}
/** Append a milestone ID to .sf/QUEUE.md, creating the file if absent. */
function _appendToQueue(sfRootPath, milestoneId) {
	const queuePath = join(sfRootPath, "QUEUE.md");
	if (existsSync(queuePath)) {
		const existing = readFileSync(queuePath, "utf-8");
		// Don't add duplicates
		if (existing.split("\n").some((l) => l.trim() === milestoneId)) return;
		const sep = existing.endsWith("\n") ? "" : "\n";
		writeFileSync(queuePath, existing + sep + milestoneId + "\n", "utf-8");
	} else {
		writeFileSync(queuePath, milestoneId + "\n", "utf-8");
	}
}
// ─── Core promotion logic ─────────────────────────────────────────────────────
/**
 * Detect actionable records in docs/records/ and promote each to a milestone.
 * Runs synchronously; silent on the no-op path. Any error is non-fatal.
 */
export function promoteActionableRecords(basePath) {
	const result = { promoted: [], skipped: [] };
	const recordsDir = join(basePath, "docs", "records");
	if (!existsSync(recordsDir)) return result;
	let entries;
	try {
		entries = readdirSync(recordsDir).filter((f) => f.endsWith(".md"));
	} catch (err) {
		logWarning("scaffold", "record-promoter: failed to read records dir", {
			error: err.message,
		});
		return result;
	}
	// Resolve the .sf root for this project
	const sfRootPath = join(basePath, ".sf");
	const milestonesPath = join(sfRootPath, "milestones");
	for (const filename of entries) {
		const recordPath = join(recordsDir, filename);
		let fm;
		try {
			fm = parseRecordFrontmatter(recordPath);
		} catch (err) {
			result.skipped.push({
				recordPath,
				reason: `parse error: ${err.message}`,
			});
			continue;
		}
		if (!fm || !fm.actionable) {
			result.skipped.push({ recordPath, reason: "not actionable" });
			continue;
		}
		if (fm.promoted) {
			result.skipped.push({ recordPath, reason: "already promoted" });
			continue;
		}
		// --- Generate milestone ---
		try {
			const milestoneId = nextMilestoneId(milestonesPath);
			const title = titleFromFilename(filename);
			const _slugBase = toSlug(
				basename(filename, ".md").replace(/^\d{4}-\d{2}-\d{2}-/, ""),
			);
			const milestoneDir = join(milestonesPath, milestoneId);
			// Read record body
			const rawContent = readFileSync(recordPath, "utf-8");
			const body = stripFrontmatter(rawContent);
			// Derive slices from H2 headings
			const headings = extractH2Headings(body);
			// Build slice table rows
			const sliceRows = headings.map((heading, i) => {
				const sId = `S${String(i + 1).padStart(2, "0")}`;
				const sSlug = toSlug(heading);
				return { id: sId, heading, slug: sSlug };
			});
			// Write milestone directory structure
			mkdirSync(milestoneDir, { recursive: true });
			// M<NNN>-CONTEXT.md — record content as context
			writeFileSync(
				join(milestoneDir, `${milestoneId}-CONTEXT.md`),
				`# ${milestoneId}: ${title} — Context\n\n**Source record:** docs/records/${filename}\n\n---\n\n${body}`,
				"utf-8",
			);
			// M<NNN>-ROADMAP.md — slices derived from H2 headings
			const sliceTable =
				sliceRows.length > 0
					? [
							"| ID | Slice | Risk | Depends | Done | After this |",
							"|----|-------|------|---------|------|------------|",
							...sliceRows.map(
								(s) =>
									`| ${s.id} | ${s.heading} | medium | — | ⬜ | Fix issues in the ${s.heading.toLowerCase()} cluster. |`,
							),
						].join("\n")
					: "_No slices derived — add H2 headings to the source record._";
			writeFileSync(
				join(milestoneDir, `${milestoneId}-ROADMAP.md`),
				[
					`# ${milestoneId}: ${title}`,
					"",
					"## Vision",
					`Address actionable findings from the ${title.toLowerCase()} record.`,
					"",
					"## Slice Overview",
					sliceTable,
					"",
				].join("\n"),
				"utf-8",
			);
			// slices/S<NN>/ directories. Slice titles live in roadmap content,
			// not path names; resolvers keep supporting legacy slug dirs.
			for (const slice of sliceRows) {
				const sliceDir = join(milestoneDir, "slices", slice.id);
				mkdirSync(sliceDir, { recursive: true });
			}
			// Write to DB backlog (primary)
			if (isDbAvailable()) {
				try {
					addBacklogItem({
						id: milestoneId,
						title: milestoneId,
						source: "record-promoter",
						status: "promoted",
					});
				} catch {
					// non-fatal
				}
			}
			// Stamp the record promoted
			stampRecordPromoted(recordPath, milestoneId);
			result.promoted.push({ recordPath, milestoneId });
		} catch (err) {
			logWarning("scaffold", "record-promoter: failed to promote record", {
				record: filename,
				error: err.message,
			});
			result.skipped.push({
				recordPath,
				reason: `promotion error: ${err.message}`,
			});
		}
	}
	return result;
}
/**
 * Fire-and-forget background dispatch of the record promoter. Returns
 * immediately; failures are non-fatal and logged via logWarning.
 */
export function dispatchRecordPromoterFireAndForget(basePath, ctx) {
	void (async () => {
		try {
			const result = promoteActionableRecords(basePath);
			if (result.promoted.length > 0 && ctx?.ui?.notify) {
				ctx.ui.notify(
					`Promoted ${result.promoted.length} actionable record(s) to milestone backlog`,
					"info",
					{
						kind: "notice",
						blocking: false,
						dedupe_key: "record-promoter",
						source: "record-promoter",
					},
				);
			}
		} catch (err) {
			logWarning("scaffold", "record-promoter dispatch failed", {
				error: err.message,
			});
		}
	})();
}
