/**
 * SF Parallel Eligibility — Milestone parallelism analysis.
 *
 * Analyzes which milestones can safely run in parallel by checking
 * dependency satisfaction and file overlap across slice plans.
 */
import { findMilestoneIds } from "./milestone-ids.js";
import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js";
import { deriveState } from "./state.js";

// Lazy import to break the cycle: parallel-eligibility ↔ parallel-orchestrator
let _parallelOrchestrator;
async function getParallelOrchestrator() {
	return (_parallelOrchestrator ??= await import("./parallel-orchestrator.js"));
}

// ─── File Collection ─────────────────────────────────────────────────────────
/**
 * Collect all `filesLikelyTouched` across every slice plan in a milestone.
 * Returns a deduplicated list of file paths.
 */
async function collectTouchedFiles(_basePath, milestoneId) {
	const files = new Set();
	if (isDbAvailable()) {
		// DB path: query slices and their tasks for file lists
		const slices = getMilestoneSlices(milestoneId);
		for (const slice of slices) {
			const tasks = getSliceTasks(milestoneId, slice.id);
			for (const task of tasks) {
				if (Array.isArray(task.files)) {
					for (const f of task.files) {
						files.add(f);
					}
				}
			}
		}
	}
	// When DB unavailable, return empty file set — parallel eligibility cannot be determined
	return [...files];
}
function collectParallelMetadataBlockers(milestoneId) {
	const blockers = [];
	if (!isDbAvailable()) return blockers;
	const slices = getMilestoneSlices(milestoneId);
	for (const slice of slices) {
		const tasks = getSliceTasks(milestoneId, slice.id);
		for (const task of tasks) {
			const frontmatter = task.frontmatter;
			if (!frontmatter) continue;
			if (frontmatter.blocksParallel) {
				blockers.push(`${slice.id}/${task.id} blocks parallel execution`);
			}
			if (frontmatter.mutationScope === "systemic") {
				blockers.push(`${slice.id}/${task.id} has systemic mutation scope`);
			}
		}
	}
	return blockers;
}
// ─── Overlap Detection ──────────────────────────────────────────────────────
/**
 * Compare file sets across milestones and return pairs with overlapping files.
 */
function detectFileOverlaps(fileSets) {
	const overlaps = [];
	const ids = [...fileSets.keys()];
	for (let i = 0; i < ids.length; i++) {
		const files1 = new Set(fileSets.get(ids[i]));
		for (let j = i + 1; j < ids.length; j++) {
			const files2 = fileSets.get(ids[j]);
			const shared = files2.filter((f) => files1.has(f));
			if (shared.length > 0) {
				overlaps.push({ mid1: ids[i], mid2: ids[j], files: shared.sort() });
			}
		}
	}
	return overlaps;
}
// ─── Analysis ────────────────────────────────────────────────────────────────
/**
 * Analyze milestones for parallel execution eligibility.
 *
 * A milestone is eligible if:
 * 1. It is not complete
 * 2. Its dependencies (`dependsOn`) are all complete
 * 3. It does not have file overlap with other eligible milestones
 *    (overlaps are flagged as warnings but do not disqualify)
 */
/**
 * Analyze which milestones are eligible for parallel execution based on deps and file overlaps.
 */
export async function analyzeParallelEligibility(basePath) {
	const milestoneIds = findMilestoneIds(basePath);
	const state = await deriveState(basePath);
	const registry = state.registry;
	// Build a lookup for quick status checks
	const registryMap = new Map();
	for (const entry of registry) {
		registryMap.set(entry.id, entry);
	}
	const eligible = [];
	const ineligible = [];
	for (const mid of milestoneIds) {
		const entry = registryMap.get(mid);
		const title = entry?.title ?? mid;
		// Rule 0: milestones with no registry entry (ghost directories, unknown
		// state) are ineligible — we cannot determine their status or deps (#2501)
		if (!entry) {
			ineligible.push({
				milestoneId: mid,
				title,
				eligible: false,
				reason:
					"Milestone has no planning data — cannot determine eligibility.",
			});
			continue;
		}
		const status = entry.status;
		// Rule 1: skip complete and parked milestones
		if (status === "complete" || status === "parked") {
			ineligible.push({
				milestoneId: mid,
				title,
				eligible: false,
				reason:
					status === "parked" ? "Milestone is parked." : "Already complete.",
			});
			continue;
		}
		// Rule 2: check dependency satisfaction
		const deps = entry.dependsOn ?? [];
		const unsatisfied = deps.filter((dep) => {
			const depEntry = registryMap.get(dep);
			return !depEntry || depEntry.status !== "complete";
		});
		if (unsatisfied.length > 0) {
			ineligible.push({
				milestoneId: mid,
				title,
				eligible: false,
				reason: `Blocked by incomplete dependencies: ${unsatisfied.join(", ")}.`,
			});
			continue;
		}
		const metadataBlockers = collectParallelMetadataBlockers(mid);
		if (metadataBlockers.length > 0) {
			ineligible.push({
				milestoneId: mid,
				title,
				eligible: false,
				reason: `Task metadata blocks parallel execution: ${metadataBlockers.join("; ")}.`,
			});
			continue;
		}
		eligible.push({
			milestoneId: mid,
			title,
			eligible: true,
			reason: "All dependencies satisfied.",
		});
	}
	// Rule 3: check file overlap among eligible milestones
	const fileSets = new Map();
	for (const result of eligible) {
		const files = await collectTouchedFiles(basePath, result.milestoneId);
		fileSets.set(result.milestoneId, files);
	}
	const fileOverlaps = detectFileOverlaps(fileSets);
	// Annotate eligible milestones that have file overlaps
	const overlappingIds = new Set();
	for (const overlap of fileOverlaps) {
		overlappingIds.add(overlap.mid1);
		overlappingIds.add(overlap.mid2);
	}
	const { getWorkerStatuses } = await getParallelOrchestrator();
	const runningWorkerIds = new Set(
		getWorkerStatuses(basePath)
			.filter((w) => w.state === "running")
			.map((w) => w.milestoneId),
	);
	for (const result of eligible) {
		if (!overlappingIds.has(result.milestoneId)) continue;
		const overlap = fileOverlaps.find(
			(o) => o.mid1 === result.milestoneId || o.mid2 === result.milestoneId,
		);
		const overlappingWith = overlap
			? overlap.mid1 === result.milestoneId
				? overlap.mid2
				: overlap.mid1
			: undefined;
		if (overlappingWith && runningWorkerIds.has(overlappingWith)) {
			result.eligible = false;
			result.reason = `File overlap with running milestone ${overlappingWith} — will dispatch after it completes.`;
		} else {
			result.reason =
				"All dependencies satisfied. NOTE: file overlap with another eligible milestone.";
		}
	}
	return { eligible, ineligible, fileOverlaps, fileSets };
}
// ─── Formatting ──────────────────────────────────────────────────────────────
/**
 * Produce a human-readable report of parallel eligibility analysis.
 */
/**
 * Format eligibility analysis results into a markdown report.
 */
export function formatEligibilityReport(candidates) {
	const lines = [];
	lines.push("# Parallel Eligibility Report");
	lines.push("");
	// Eligible milestones
	lines.push(
		`## Eligible for Parallel Execution (${candidates.eligible.length})`,
	);
	lines.push("");
	if (candidates.eligible.length === 0) {
		lines.push("No milestones are currently eligible for parallel execution.");
	} else {
		for (const e of candidates.eligible) {
			lines.push(`- **${e.milestoneId}** — ${e.title}`);
			lines.push(`  ${e.reason}`);
		}
	}
	lines.push("");
	// Ineligible milestones
	lines.push(`## Ineligible (${candidates.ineligible.length})`);
	lines.push("");
	if (candidates.ineligible.length === 0) {
		lines.push("All milestones are eligible.");
	} else {
		for (const e of candidates.ineligible) {
			lines.push(`- **${e.milestoneId}** — ${e.title}`);
			lines.push(`  ${e.reason}`);
		}
	}
	lines.push("");
	// File overlap warnings
	if (candidates.fileOverlaps.length > 0) {
		lines.push(`## File Overlap Warnings (${candidates.fileOverlaps.length})`);
		lines.push("");
		for (const overlap of candidates.fileOverlaps) {
			lines.push(
				`- **${overlap.mid1}** <-> **${overlap.mid2}** — ${overlap.files.length} shared file(s):`,
			);
			for (const f of overlap.files) {
				lines.push(`  - \`${f}\``);
			}
		}
		lines.push("");
	}
	return lines.join("\n");
}
