/**
 * SF Queue Order — Custom milestone execution ordering.
 *
 * Stores explicit execution order in the `milestones.sequence` DB column when
 * SQLite is available. `.sf/QUEUE-ORDER.json` is read/written only as a legacy
 * fallback when the database is unavailable.
 */
import { join } from "node:path";
import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
import { milestoneIdSort } from "./milestone-id-sort.js";
import { sfRoot } from "./paths.js";
import {
	getAllMilestones,
	isDbAvailable,
	updateMilestoneQueueOrder,
} from "./sf-db.js";

// ─── Path ────────────────────────────────────────────────────────────────────
function queueOrderPath(basePath) {
	return join(sfRoot(basePath), "QUEUE-ORDER.json");
}
// ─── Type Guards ─────────────────────────────────────────────────────────────
function isQueueOrderFile(data) {
	return (
		data !== null &&
		typeof data === "object" &&
		"order" in data &&
		Array.isArray(data.order)
	);
}
// ─── Read / Write ────────────────────────────────────────────────────────────
/**
 * Load the custom queue order. Returns null if no DB order or legacy fallback
 * exists.
 */
export function loadQueueOrder(basePath) {
	if (isDbAvailable()) {
		const milestones = getAllMilestones();
		if (milestones.some((m) => (m.sequence ?? 0) > 0)) {
			return milestones
				.filter((m) => (m.sequence ?? 0) > 0)
				.sort(
					(a, b) =>
						(a.sequence ?? 0) - (b.sequence ?? 0) || a.id.localeCompare(b.id),
				)
				.map((m) => m.id);
		}
	}
	const data = loadJsonFileOrNull(queueOrderPath(basePath), isQueueOrderFile);
	return data?.order ?? null;
}
/**
 * Save a custom queue order to the DB, falling back to legacy JSON only when
 * SQLite is unavailable.
 */
export function saveQueueOrder(basePath, order) {
	if (isDbAvailable()) {
		updateMilestoneQueueOrder(order);
		return;
	}
	const data = {
		order,
		updatedAt: new Date().toISOString(),
	};
	saveJsonFile(queueOrderPath(basePath), data);
}
// ─── Sorting ─────────────────────────────────────────────────────────────────
/**
 * Sort milestone IDs respecting a custom order.
 *
 * - IDs present in `customOrder` appear in that exact sequence.
 * - IDs on disk but NOT in `customOrder` are appended at the end,
 *   sorted by the default `milestoneIdSort` (numeric).
 * - IDs in `customOrder` but NOT on disk are silently skipped.
 * - When `customOrder` is null, falls back to `milestoneIdSort`.
 */
export function sortByQueueOrder(ids, customOrder) {
	if (!customOrder) return [...ids].sort(milestoneIdSort);
	const idSet = new Set(ids);
	const ordered = [];
	// First: IDs from customOrder that exist on disk
	for (const id of customOrder) {
		if (idSet.has(id)) {
			ordered.push(id);
			idSet.delete(id);
		}
	}
	// Then: remaining IDs not in customOrder, in default sort order
	const remaining = [...idSet].sort(milestoneIdSort);
	return [...ordered, ...remaining];
}
// ─── Pruning ─────────────────────────────────────────────────────────────────
/**
 * Remove IDs from the queue order file that are no longer valid
 * (completed or deleted milestones). No-op if file doesn't exist.
 */
export function pruneQueueOrder(basePath, validIds) {
	const order = loadQueueOrder(basePath);
	if (!order) return;
	const validSet = new Set(validIds);
	const pruned = order.filter((id) => validSet.has(id));
	if (pruned.length !== order.length) {
		saveQueueOrder(basePath, pruned);
	}
}
// ─── Validation ──────────────────────────────────────────────────────────────
/**
 * Validate a proposed queue order against dependency constraints.
 *
 * Checks:
 * - would_block: A milestone is placed before one of its dependencies
 * - circular: Two or more milestones form a dependency cycle
 * - missing_dep: A milestone depends on an ID that doesn't exist
 * - redundant: A dependency is satisfied by queue position (dep comes earlier)
 */
export function validateQueueOrder(order, depsMap, completedIds) {
	const violations = [];
	const redundant = [];
	const positionMap = new Map();
	for (let i = 0; i < order.length; i++) {
		positionMap.set(order[i], i);
	}
	const allKnownIds = new Set([...order, ...completedIds]);
	for (const [mid, deps] of depsMap) {
		const midPos = positionMap.get(mid);
		if (midPos === undefined) continue; // not in pending order
		for (const dep of deps) {
			// Dep already completed — always satisfied
			if (completedIds.has(dep)) continue;
			// Dep doesn't exist anywhere
			if (!allKnownIds.has(dep)) {
				violations.push({
					milestone: mid,
					dependsOn: dep,
					type: "missing_dep",
					message: `${mid} depends on ${dep}, but ${dep} does not exist.`,
				});
				continue;
			}
			const depPos = positionMap.get(dep);
			if (depPos === undefined) continue; // dep not in pending order (edge case)
			if (depPos > midPos) {
				// Dep comes AFTER this milestone in the order — violation
				violations.push({
					milestone: mid,
					dependsOn: dep,
					type: "would_block",
					message: `${mid} cannot run before ${dep} — ${mid} depends_on: [${dep}].`,
				});
			} else {
				// Dep comes before — satisfied by position, redundant
				redundant.push({ milestone: mid, dependsOn: dep });
			}
		}
	}
	// Check for circular dependencies
	const visited = new Set();
	const inStack = new Set();
	function hasCycle(node, path) {
		if (inStack.has(node)) return [...path, node];
		if (visited.has(node)) return null;
		visited.add(node);
		inStack.add(node);
		const deps = depsMap.get(node) ?? [];
		for (const dep of deps) {
			if (completedIds.has(dep)) continue;
			const cycle = hasCycle(dep, [...path, node]);
			if (cycle) return cycle;
		}
		inStack.delete(node);
		return null;
	}
	for (const mid of order) {
		if (!visited.has(mid)) {
			const cycle = hasCycle(mid, []);
			if (cycle) {
				const cycleStr = cycle.join(" → ");
				violations.push({
					milestone: cycle[0],
					dependsOn: cycle[cycle.length - 2],
					type: "circular",
					message: `Circular dependency: ${cycleStr}`,
				});
				break; // one cycle report is enough
			}
		}
	}
	return {
		valid: violations.length === 0,
		violations,
		redundant,
	};
}
