/**
 * Milestone ID primitives — pure utilities for generating, parsing, sorting,
 * and discovering milestone identifiers.
 *
 * Consumed by 15+ modules across the SF extension. Zero side-effects.
 */
import { randomInt } from "node:crypto";
import { existsSync, readdirSync } from "node:fs";
import { getErrorMessage } from "./error-utils.js";
import { extractMilestoneSeq, milestoneIdSort } from "./milestone-id-sort.js";
import { milestonesDir } from "./paths.js";
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
import { getAllMilestones } from "./sf-db.js";
import { logWarning } from "./workflow-logger.js";
// ─── Regex ──────────────────────────────────────────────────────────────────
/** Matches both classic `M001` and unique `M001-abc123` formats (anchored). */
export const MILESTONE_ID_RE = /^M\d{3}(?:-[a-z0-9]{6})?$/;
// Re-export sort helpers for consumers that import them from this module.
export { extractMilestoneSeq, milestoneIdSort };
// ─── Parsing & Extraction ───────────────────────────────────────────────────
/** Structured parse of a milestone ID into optional suffix and sequence number. */
export function parseMilestoneId(id) {
	const m = id.match(/^M(\d{3})(?:-([a-z0-9]{6}))?$/);
	if (!m) return { num: 0 };
	return {
		...(m[2] ? { suffix: m[2] } : {}),
		num: parseInt(m[1], 10),
	};
}
// ─── Generation ─────────────────────────────────────────────────────────────
/** Generate a 6-char lowercase `[a-z0-9]` suffix using crypto.randomInt(). */
export function generateMilestoneSuffix() {
	const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
	let result = "";
	for (let i = 0; i < 6; i++) {
		result += chars[randomInt(36)];
	}
	return result;
}
/** Return the highest numeric suffix among milestone IDs (0 when the list is empty or has no numeric IDs). */
export function maxMilestoneNum(milestoneIds) {
	return milestoneIds.reduce((max, id) => {
		const num = extractMilestoneSeq(id);
		return num > max ? num : max;
	}, 0);
}
/** Derive the next milestone ID from existing IDs using max-based approach to avoid collisions after deletions. */
export function nextMilestoneId(milestoneIds, uniqueEnabled) {
	const seq = String(maxMilestoneNum(milestoneIds) + 1).padStart(3, "0");
	if (uniqueEnabled) {
		return `M${seq}-${generateMilestoneSuffix()}`;
	}
	return `M${seq}`;
}
// ─── Reservation ─────────────────────────────────────────────────────────────
/**
 * Module-level set of milestone IDs that have been previewed/promised to the
 * user but not yet materialised on disk. Both guided-flow (preview) and
 * new_milestone_id (tool) share this set so the ID shown in the UI
 * matches the one the tool returns.
 */
const reservedMilestoneIds = new Set();
/** Reserve an ID so that subsequent calls to `claimReservedId` / `nextMilestoneId` account for it. */
export function reserveMilestoneId(id) {
	reservedMilestoneIds.add(id);
}
/**
 * If any IDs have been reserved, shift one out and return it.
 * Returns `undefined` when the reservation set is empty.
 */
export function claimReservedId() {
	const first = reservedMilestoneIds.values().next().value;
	if (first !== undefined) {
		reservedMilestoneIds.delete(first);
		return first;
	}
	return undefined;
}
/** Return a snapshot of all currently reserved IDs (for merging into the "existing" list). */
export function getReservedMilestoneIds() {
	return reservedMilestoneIds;
}
/** Clear all reservations (useful for tests). */
export function clearReservedMilestoneIds() {
	reservedMilestoneIds.clear();
}
// ─── Discovery ──────────────────────────────────────────────────────────────
/**
 * Return milestone IDs, DB-first.
 *
 * When the DB is open, reads from the `milestones` table — the canonical
 * source of truth — so stale or duplicated filesystem dirs (e.g. both
 * `M001/` and `M001-6377a4/`) are never returned.
 * Falls back to a filesystem scan only when the DB is not yet open (early
 * bootstrap or first-init before `ensureDbOpen`).
 */
export function findMilestoneIds(basePath) {
	// DB-first: avoids returning duplicate/legacy dirs from filesystem
	try {
		const dbRows = getAllMilestones();
		if (dbRows.length > 0) {
			const ids = dbRows.map((m) => m.id);
			const customOrder = loadQueueOrder(basePath);
			return sortByQueueOrder(ids, customOrder);
		}
	} catch {
		// DB not open yet — fall through to filesystem scan
	}
	// Filesystem fallback (early bootstrap / first-init)
	const dir = milestonesDir(basePath);
	try {
		const ids = readdirSync(dir, { withFileTypes: true })
			.filter((d) => d.isDirectory())
			.map((d) => {
				const match = d.name.match(/^(M\d+(?:-[a-z0-9]{6})?)/);
				return match ? match[1] : null;
			})
			.filter((id) => id !== null);
		// Apply custom queue order if available, else fall back to numeric sort
		const customOrder = loadQueueOrder(basePath);
		return sortByQueueOrder(ids, customOrder);
	} catch (err) {
		// Log why milestone scanning failed — silent [] here causes infinite loops (#456)
		if (existsSync(dir)) {
			logWarning(
				"engine",
				`findMilestoneIds: .sf/milestones/ exists but readdirSync failed — ${getErrorMessage(err)}`,
			);
		}
		return [];
	}
}
