/**
 * 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 the requirements DB table.
 *
 * Requirements flow into prompt context via the existing planning pipeline,
 * so promotion turns recurring noise into structured requirements the next
 * planning round naturally addresses.
 *
 * Consumer: session_start drain hook in register-hooks.ts (wired separately).
 */
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { markResolved, readAllSelfFeedback } from "./self-feedback.js";
import {
	getActiveRequirements,
	isDbAvailable,
	upsertRequirement,
} from "./sf-db.js";

// ─── Constants ───────────────────────────────────────────────────────────────
const COUNT_THRESHOLD = 5;
const MILESTONE_THRESHOLD = 3;
const LOOKBACK_DAYS = 90;
// ─── 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;
	}
}
/**
 * Return the highest R-number present in the requirements DB table.
 * Returns 0 when the DB is unavailable or contains no R-IDs.
 */
function readHighestRNumber() {
	try {
		if (!isDbAvailable()) return 0;
		const rows = getActiveRequirements();
		let max = 0;
		for (const row of rows) {
			const m = /\bR(\d+)\b/.exec(row.id ?? "");
			if (m) {
				const n = parseInt(m[1], 10);
				if (n > max) max = n;
			}
		}
		return max;
	} catch {
		return 0;
	}
}
/**
 * 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 PREFIX (domain:family from the canonical
		// domain:family[:specific] taxonomy enforced at recordSelfFeedback in
		// commit 83c28b756 — sf-mp4rxkwt-sfthez). Without this, every entry
		// has a unique full kind string and clusters are size-1 forever, so
		// the promoter can never fire (verified empirically on the live
		// ledger 2026-05-14: 10 open entries, max cluster size 1 under
		// exact-string matching). Using the first two segments groups e.g.
		// gap:routing:tiebreak-cost-only with gap:routing:agentic-axis-
		// partial-coverage as one "gap:routing" cluster while still
		// distinguishing them from gap:upstream-bridge-no-closure-memory.
		// Single-segment legacy kinds are clustered as themselves (no
		// prefix to extract).
		const clusters = new Map();
		for (const e of eligible) {
			const segments = (e.kind ?? "").split(":");
			const clusterKey =
				segments.length >= 2 ? `${segments[0]}:${segments[1]}` : (e.kind ?? "");
			const existing = clusters.get(clusterKey);
			if (existing) {
				existing.entries.push(e);
				existing.distinctMilestones.add(e.occurredIn?.milestone);
			} else {
				clusters.set(clusterKey, {
					kind: clusterKey,
					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;
		let highestNum = readHighestRNumber();
		const promotedIds = [];
		for (const cluster of promotable) {
			const nextNum = ++highestNum;
			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}`;
			if (isDbAvailable()) {
				try {
					// upsertRequirement binds every column positionally, so all
					// fields the schema names must be supplied — undefined cannot
					// bind to SQLite. Latent bug observed during AC4 work for
					// sf-mp4rxkwt-sfthez: the promoter has been silently swallowing
					// upsert failures in the try/catch below, which is why it
					// "never fired" in production — the upsert was throwing on
					// every attempt because partial fields were left undefined.
					// `title` is computed for log/notify but the requirements
					// schema has no title column; we encode it as the first line
					// of `description` so it survives.
					upsertRequirement({
						id: reqId,
						class: "operational",
						status: "active",
						description: `${title}\n\n${notes}`,
						why: `Threshold reached: ${count} entr${count === 1 ? "y" : "ies"} of kind cluster ${cluster.kind} ${milestoneCount > 1 ? `across ${milestoneCount} milestones` : ""}`.trim(),
						source: "sf-promoter",
						primary_owner: null,
						supporting_slices: null,
						validation: null,
						notes,
						full_content: null,
						superseded_by: null,
					});
				} catch {
					// non-fatal
				}
			}
			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;
	}
}
