// SF Dispatch Guard — prevents out-of-order slice dispatch
import { readFileSync } from "node:fs";
import { findMilestoneIds } from "./milestone-ids.js";
import { classifyMilestoneSummaryContent } from "./milestone-summary-classifier.js";
import { parseRoadmap } from "./parsers.js";
import { resolveMilestoneFile } from "./paths.js";
import { getMilestone, getMilestoneSlices, isDbAvailable } from "./sf-db.js";
import { isClosedStatus } from "./status-guards.js";
import { parseUnitId } from "./unit-id.js";

const SLICE_DISPATCH_TYPES = new Set([
	"research-slice",
	"plan-slice",
	"replan-slice",
	"execute-task",
	"complete-slice",
]);
/**
 * Check if a slice/task dispatch should be blocked by incomplete prior slices.
 * Returns error message if blocked, null if dispatch is safe.
 * Respects milestone locking (SF_MILESTONE_LOCK) for parallel worker isolation.
 */
export function getPriorSliceCompletionBlocker(
	base,
	_mainBranch,
	unitType,
	unitId,
) {
	if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
	const { milestone: targetMid, slice: targetSid } = parseUnitId(unitId);
	if (!targetMid || !targetSid) return null;
	// Parallel worker isolation: when SF_MILESTONE_LOCK is set, this worker
	// is scoped to a single milestone. Skip the cross-milestone dependency
	// check — other milestones are being handled by their own workers.
	// Without this, the dispatch guard sees incomplete slices in M010/M011
	// (cloned into the worktree DB) and blocks M012 from ever starting. #2797
	const milestoneLock = process.env.SF_MILESTONE_LOCK;
	// Use findMilestoneIds to respect custom queue order.
	// Only check milestones that come BEFORE the target in queue order.
	// When locked to a specific milestone, only check that milestone's
	// intra-slice dependencies — skip all cross-milestone checks.
	const allIds =
		milestoneLock && targetMid === milestoneLock
			? [targetMid]
			: findMilestoneIds(base);
	const targetIdx = allIds.indexOf(targetMid);
	if (targetIdx < 0) return null;
	const milestoneIds = allIds.slice(0, targetIdx + 1);
	for (const mid of milestoneIds) {
		if (resolveMilestoneFile(base, mid, "PARKED")) continue;
		// DB-first: check milestone status authoritatively; only skip if DB confirms closed.
		// Filesystem fallback: read SUMMARY and classify — skip only if it's a success/unknown
		// summary, NOT a failure summary. Fail-closed: unreadable SUMMARY does not count as done.
		if (isDbAvailable()) {
			const milestoneRow = getMilestone(mid);
			if (milestoneRow && isClosedStatus(milestoneRow.status)) continue;
		} else {
			const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
			if (summaryPath) {
				let summaryContent = null;
				try {
					summaryContent = readFileSync(summaryPath, "utf-8");
				} catch {
					// unreadable — fail-closed: don't treat as complete
				}
				if (
					summaryContent != null &&
					classifyMilestoneSummaryContent(summaryContent) !== "failure"
				) {
					continue;
				}
			}
		}
		let slices = null;
		if (isDbAvailable()) {
			const rows = getMilestoneSlices(mid);
			if (rows.length > 0) {
				slices = rows.map((r) => ({
					id: r.id,
					done: isClosedStatus(r.status),
					depends: r.depends ?? [],
				}));
			}
		}
		if (!slices) {
			// Unmigrated/recovery fallback: parse rendered roadmap checkboxes only
			// when the DB has no slice rows for this milestone.
			const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
			if (!roadmapPath) continue;
			let roadmapContent;
			try {
				roadmapContent = readFileSync(roadmapPath, "utf-8");
			} catch {
				continue;
			}
			const parsed = parseRoadmap(roadmapContent);
			if (parsed.slices.length === 0) continue;
			slices = parsed.slices.map((s) => ({
				id: s.id,
				done: s.done,
				depends: s.depends ?? [],
			}));
		}
		if (mid !== targetMid) {
			const incomplete = slices.find((slice) => !slice.done);
			if (incomplete) {
				return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${mid}/${incomplete.id} is not complete.`;
			}
			continue;
		}
		const targetSlice = slices.find((slice) => slice.id === targetSid);
		if (!targetSlice) return null;
		// Dependency-aware ordering: if the target slice declares dependencies,
		// only require those specific slices to be complete — not all positionally
		// earlier slices.  This prevents deadlocks when a positionally-earlier
		// slice depends on a positionally-later one (e.g. S05 depends_on S06).
		//
		// When the target has NO declared dependencies, fall back to the original
		// positional ordering for backward compatibility.
		if (targetSlice.depends.length > 0) {
			const sliceMap = new Map(slices.map((s) => [s.id, s]));
			for (const depId of targetSlice.depends) {
				const dep = sliceMap.get(depId);
				if (dep && !dep.done) {
					return `Cannot dispatch ${unitType} ${unitId}: dependency slice ${targetMid}/${depId} is not complete.`;
				}
				// If dep is not found in this milestone's slices, ignore it —
				// it may be a cross-milestone reference handled elsewhere.
			}
		} else {
			// Positional fallback is only a heuristic for legacy slices with no
			// declared dependencies. Skip any earlier slice that depends on the
			// target, directly or transitively, or we can deadlock a valid zero-dep
			// slice behind its own downstream dependents (#3720).
			//
			// Also skip incomplete earlier slices that have unsatisfied dependencies
			// of their own — those slices are legitimately stuck and should not
			// block a zero-dep slice that is ready to run. This scopes the
			// positional check to the target slice only, rather than applying the
			// global milestone-has-explicit-deps short-circuit that was here
			// previously (#3998).
			const sliceMap = new Map(slices.map((s) => [s.id, s]));
			const reverseDependents = new Set();
			let changed = true;
			while (changed) {
				changed = false;
				for (const slice of slices) {
					if (reverseDependents.has(slice.id)) continue;
					if (
						slice.depends.some(
							(depId) => depId === targetSid || reverseDependents.has(depId),
						)
					) {
						reverseDependents.add(slice.id);
						changed = true;
					}
				}
			}
			const hasUnsatisfiedDeps = (slice) =>
				slice.depends.some((depId) => {
					const dep = sliceMap.get(depId);
					return dep !== undefined && !dep.done;
				});
			const targetIndex = slices.findIndex((slice) => slice.id === targetSid);
			const incomplete = slices
				.slice(0, targetIndex)
				.find(
					(slice) =>
						!slice.done &&
						!reverseDependents.has(slice.id) &&
						!hasUnsatisfiedDeps(slice),
				);
			if (incomplete) {
				return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${targetMid}/${incomplete.id} is not complete.`;
			}
		}
	}
	return null;
}
