// SF Extension — State Derivation
// DB-primary state derivation with explicit recovery guidance when DB-backed
// projects cannot be opened. Legacy filesystem parsing remains available only
// for projects that have not yet attempted DB bootstrap in this process.

import { debugCount, debugTime } from "./debug-logger.js";
import { loadFile } from "./files.js";
import { findMilestoneIds } from "./milestone-ids.js";
import { parseRoadmap } from "./parsers.js";
import {
	clearPathCache,
	resolveMilestoneFile,
	resolveSfRootFile,
} from "./paths.js";
import { loadQueueOrder, sortByQueueOrder } from "./queue-order.js";
import {
	getAllMilestones,
	isDbAvailable,
	wasDbOpenAttempted,
} from "./sf-db.js";
import { deriveStateFromDb } from "./state-db.js";
import { _deriveStateImpl } from "./state-legacy.js";
// Sub-module imports
import {
	extractContextTitle,
	isGhostMilestone,
	isMilestoneComplete,
	isSliceComplete,
	isValidationTerminal,
	loadTerminalSummary,
	readMilestoneValidationVerdict,
	stripMilestonePrefix,
} from "./state-shared.js";
import { isClosedStatus } from "./status-guards.js";
import { logWarning } from "./workflow-logger.js";

export {
	_deriveStateImpl,
	deriveStateFromDb,
	extractContextTitle,
	isGhostMilestone,
	isMilestoneComplete,
	isSliceComplete,
	isValidationTerminal,
	loadTerminalSummary,
	readMilestoneValidationVerdict,
	stripMilestonePrefix,
};

const CACHE_TTL_MS = 5000;
let _stateCache = null;
// ── Telemetry counters for derive-path observability ────────────────────────
const _telemetry = { dbDeriveCount: 0, markdownDeriveCount: 0 };
const EMPTY_REQUIREMENT_COUNTS = Object.freeze({
	active: 0,
	validated: 0,
	deferred: 0,
	outOfScope: 0,
	blocked: 0,
	total: 0,
});

function hasLegacyRuntimeArtifacts(basePath) {
	if (findMilestoneIds(basePath).length > 0) return true;
	return (
		resolveSfRootFile(basePath, "PROJECT") !== null ||
		resolveSfRootFile(basePath, "REQUIREMENTS") !== null ||
		resolveSfRootFile(basePath, "DECISIONS") !== null ||
		resolveSfRootFile(basePath, "KNOWLEDGE") !== null ||
		resolveSfRootFile(basePath, "RUNTIME") !== null ||
		resolveSfRootFile(basePath, "STATE") !== null
	);
}

function buildDbRecoveryRequiredState() {
	return {
		activeMilestone: null,
		activeSlice: null,
		activeTask: null,
		phase: "blocked",
		recentDecisions: [],
		blockers: [
			"DB-backed SF state is unavailable. Runtime does not fall back to markdown authority after DB bootstrap fails.",
		],
		nextAction:
			"Run `sf recover` to rebuild DB state from disk, or `sf migrate` for a legacy markdown-only project.",
		registry: [],
		requirements: EMPTY_REQUIREMENT_COUNTS,
		progress: { milestones: { done: 0, total: 0 } },
	};
}

/**
 * Load SUMMARY content for a milestone and return it only if it is readable
 * AND classifies as terminal (success/unknown, not failure).
 *
 * Purpose: centralise the fail-closed SUMMARY check so the pattern
 * "only treat a SUMMARY as terminal if we can read it AND it passes the
 * classifier" cannot drift between the five call sites in state derivation.
 * Returning null on either an unreadable file or a failure summary ensures
 * that a corrupt or failure SUMMARY never silently marks a milestone complete.
 *
 * Consumer: getActiveMilestoneId() and _deriveStateImpl() Phase 1 + Phase 2.
 */

export function invalidateStateCache() {
	_stateCache = null;
	clearPathCache();
}
/**
 * Returns the ID of the first incomplete milestone, or null if all are complete.
 */
export async function getActiveMilestoneId(basePath) {
	// Parallel worker isolation
	const milestoneLock = process.env.SF_MILESTONE_LOCK;
	if (milestoneLock) {
		const milestoneIds = findMilestoneIds(basePath);
		if (!milestoneIds.includes(milestoneLock)) return null;
		const lockedParked = resolveMilestoneFile(
			basePath,
			milestoneLock,
			"PARKED",
		);
		if (lockedParked) return null;
		return milestoneLock;
	}
	// DB-first: query milestones table for the first non-complete, non-parked milestone
	if (isDbAvailable()) {
		const allMilestones = getAllMilestones();
		if (allMilestones.length > 0) {
			// Respect queue-order.json so /queue reordering is honored (#2556).
			// Without this, the DB path uses lexicographic sort while the dispatch
			// guard uses queue order — causing a deadlock.
			const customOrder = loadQueueOrder(basePath);
			const sortedIds = sortByQueueOrder(
				allMilestones.map((m) => m.id),
				customOrder,
			);
			const byId = new Map(allMilestones.map((m) => [m.id, m]));
			for (const id of sortedIds) {
				const m = byId.get(id);
				if (isClosedStatus(m.status) || m.status === "parked") continue;
				return m.id;
			}
			return null;
		}
	}
	// Filesystem fallback for unmigrated projects or empty DB
	const milestoneIds = findMilestoneIds(basePath);
	for (const mid of milestoneIds) {
		const parkedFile = resolveMilestoneFile(basePath, mid, "PARKED");
		if (parkedFile) continue;
		const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
		const content = roadmapFile ? await loadFile(roadmapFile) : null;
		if (!content) {
			const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
			if (await loadTerminalSummary(summaryFile, loadFile)) continue;
			if (isGhostMilestone(basePath, mid)) continue;
			return mid;
		}
		const roadmap = parseRoadmap(content);
		if (!isMilestoneComplete(roadmap)) {
			const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
			if (await loadTerminalSummary(summaryFile, loadFile)) continue;
			return mid;
		}
	}
	return null;
}
/**
 * Reconstruct SF state from DB (primary) or filesystem (fallback).
 * STATE.md is a rendered cache of this output.
 *
 * When DB is available, queries milestone/slice/task tables directly.
 * Falls back to filesystem parsing only when DB is unavailable.
 */
export async function deriveState(basePath) {
	// Return cached result if within the TTL window for the same basePath
	if (
		_stateCache &&
		_stateCache.basePath === basePath &&
		Date.now() - _stateCache.timestamp < CACHE_TTL_MS
	) {
		return _stateCache.result;
	}
	const stopTimer = debugTime("derive-state-impl");
	let result;
	// Dual-path: try DB-backed derivation first when hierarchy tables are populated
	if (isDbAvailable()) {
		const stopDbTimer = debugTime("derive-state-db");
		result = await deriveStateFromDb(basePath);
		stopDbTimer({
			phase: result.phase,
			milestone: result.activeMilestone?.id,
		});
		_telemetry.dbDeriveCount++;
	} else {
		// Only warn when DB initialization was attempted and failed — not when
		// the DB simply hasn't been opened yet (e.g. during before_agent_start
		// context injection which runs before any tool invocation opens the DB).
		if (wasDbOpenAttempted()) {
			if (hasLegacyRuntimeArtifacts(basePath)) {
				logWarning(
					"state",
					"DB unavailable for a project with legacy SF artifacts — refusing runtime markdown fallback; run sf recover or sf migrate",
				);
				result = buildDbRecoveryRequiredState();
				stopTimer({
					phase: result.phase,
					milestone: result.activeMilestone?.id,
				});
				debugCount("deriveStateCalls");
				_stateCache = { basePath, result, timestamp: Date.now() };
				return result;
			}
			logWarning("state", "DB unavailable — using filesystem state derivation");
		}
		result = await _deriveStateImpl(basePath);
		_telemetry.markdownDeriveCount++;
	}
	stopTimer({ phase: result.phase, milestone: result.activeMilestone?.id });
	debugCount("deriveStateCalls");
	_stateCache = { basePath, result, timestamp: Date.now() };
	return result;
}
/**
 * Extract milestone title from CONTEXT.md or CONTEXT-DRAFT.md heading.
 * Falls back to the provided fallback (usually the milestone ID).
 */
/**
 * Strip the "M001: " prefix from a milestone title to get the human-readable name.
 * Used by both DB and filesystem paths for consistency.
 */
