/**
 * Requirement Promoter — threshold-to-requirement promotion sweeper.
 *
 * When feedback entries cluster (e.g., 5 instances of `git-empty-pathspec`,
 * or `runaway-guard-hard-pause` recurring across 3 milestones), this module
 * auto-promotes to a row in `.sf/REQUIREMENTS.md`.
 *
 * Requirements flow into prompt context via the existing planning pipeline,
 * so promotion turns "noise that piles up in SELF-FEEDBACK.md" into "something
 * the next planning round naturally addresses."
 *
 * Consumer: session_start drain hook in register-hooks.ts (wired separately).
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { sfRoot } from "./paths.js";
import { markResolved, readAllSelfFeedback } from "./self-feedback.js";

// ─── Constants ───────────────────────────────────────────────────────────────
const COUNT_THRESHOLD = 5;
const MILESTONE_THRESHOLD = 3;
const LOOKBACK_DAYS = 90;
const REQUIREMENTS_HEADER =
	"# Requirements\n\n" +
	"This file is the explicit capability and coverage contract for the project.\n\n" +
	"## Active\n\n";
// ─── Forge detection (local — isForgeRepo is not exported) ───────────────────
function isForgeRepo(basePath) {
	try {
		const pkgPath = join(basePath, "package.json");
		if (!existsSync(pkgPath)) return false;
		const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
		return pkg?.name === "singularity-forge";
	} catch {
		return false;
	}
}
// ─── REQUIREMENTS.md helpers ─────────────────────────────────────────────────
function requirementsPath(basePath) {
	return join(sfRoot(basePath), "REQUIREMENTS.md");
}
/**
 * Read the highest R-number present in REQUIREMENTS.md.
 * Returns 0 if the file is absent or contains no R-IDs.
 */
function readHighestRNumber(filePath) {
	try {
		if (!existsSync(filePath)) return 0;
		const content = readFileSync(filePath, "utf-8");
		const matches = content.matchAll(/\bR(\d+)\b/g);
		let max = 0;
		for (const m of matches) {
			const n = parseInt(m[1], 10);
			if (n > max) max = n;
		}
		return max;
	} catch {
		return 0;
	}
}
/**
 * Append one requirement block to REQUIREMENTS.md.
 * Creates the file with header if it does not exist.
 * Appends into the ## Active section (or at end if already structured).
 */
function appendRequirementRow(filePath, id, title, notes) {
	const dir = join(filePath, "..");
	if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
	const block =
		`### ${id} — ${title}\n` +
		`- Class: operational\n` +
		`- Status: active\n` +
		`- Description: ${title}\n` +
		`- Source: sf-promoter\n` +
		`- Notes: ${notes}\n\n`;
	if (!existsSync(filePath)) {
		writeFileSync(filePath, REQUIREMENTS_HEADER + block, "utf-8");
	} else {
		// Append before any ## Traceability or ## Coverage Summary section if
		// present; otherwise just append at the end.
		const content = readFileSync(filePath, "utf-8");
		const insertionMarker = content.match(
			/\n## (?:Traceability|Coverage Summary)/,
		);
		if (insertionMarker && insertionMarker.index !== undefined) {
			const before = content.slice(0, insertionMarker.index);
			const after = content.slice(insertionMarker.index);
			writeFileSync(filePath, before + "\n" + block + after, "utf-8");
		} else {
			const appended = content.endsWith("\n")
				? content + block
				: content + "\n" + block;
			writeFileSync(filePath, appended, "utf-8");
		}
	}
}
/**
 * Promote feedback entries to REQUIREMENTS.md when they cross threshold.
 *
 * Only runs when basePath is the forge repo itself. Bails silently otherwise.
 * Never throws — returns { promoted: 0, requirementIds: [] } on failure.
 *
 * @returns { promoted: number; requirementIds: string[] }
 */
export function promoteFeedbackToRequirements(basePath = process.cwd()) {
	const empty = { promoted: 0, requirementIds: [] };
	try {
		// Gate: only runs on singularity-forge itself
		if (!isForgeRepo(basePath)) return empty;
		const cutoff = new Date(Date.now() - LOOKBACK_DAYS * 24 * 60 * 60 * 1000);
		// Read all entries, filter to open forge entries within the lookback window
		const eligible = readAllSelfFeedback(basePath).filter(
			(e) =>
				!e.resolvedAt && e.repoIdentity === "forge" && new Date(e.ts) >= cutoff,
		);
		if (eligible.length === 0) return empty;
		// Cluster by kind
		const clusters = new Map();
		for (const e of eligible) {
			const existing = clusters.get(e.kind);
			if (existing) {
				existing.entries.push(e);
				existing.distinctMilestones.add(e.occurredIn?.milestone);
			} else {
				clusters.set(e.kind, {
					kind: e.kind,
					entries: [e],
					distinctMilestones: new Set([e.occurredIn?.milestone]),
				});
			}
		}
		// Determine which clusters cross the promotion threshold
		const promotable = [...clusters.values()].filter(
			(c) =>
				c.entries.length >= COUNT_THRESHOLD ||
				c.distinctMilestones.size >= MILESTONE_THRESHOLD,
		);
		if (promotable.length === 0) return empty;
		const reqPath = requirementsPath(basePath);
		const promotedIds = [];
		for (const cluster of promotable) {
			const nextNum = readHighestRNumber(reqPath) + 1;
			const reqId = `R${String(nextNum).padStart(3, "0")}`;
			const count = cluster.entries.length;
			const milestoneCount = cluster.distinctMilestones.size;
			const title = `Address recurring ${cluster.kind} (${count} entries across ${milestoneCount} milestone${milestoneCount !== 1 ? "s" : ""})`;
			const sourceIds = cluster.entries.map((e) => e.id).join(", ");
			const notes = `Source IDs: ${sourceIds}`;
			appendRequirementRow(reqPath, reqId, title, notes);
			promotedIds.push(reqId);
			// Mark each contributing entry resolved
			for (const entry of cluster.entries) {
				markResolved(
					entry.id,
					{
						reason: `Promoted to requirement ${reqId} by threshold-promotion sweeper`,
						evidence: { kind: "promoted-to-requirement", requirementId: reqId },
					},
					basePath,
				);
			}
		}
		return { promoted: promotedIds.length, requirementIds: promotedIds };
	} catch {
		return empty;
	}
}
