/**
 * canonical-milestone-plan.js - canonical read accessor for milestone plans.
 *
 * Purpose: give dispatch-facing code one bounded way to read milestone slice
 * state without treating rendered ROADMAP.md as executable state.
 */
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { getErrorMessage } from "./error-utils.js";
import {
	getDbPath,
	getMilestone,
	getMilestoneSlices,
	isDbAvailable,
	openDatabase,
	readTransaction,
} from "./sf-db.js";

function milestoneDir(basePath, milestoneId) {
	return join(basePath, ".sf", "milestones", milestoneId);
}

function roadmapJsonPath(basePath, milestoneId) {
	return join(
		milestoneDir(basePath, milestoneId),
		`${milestoneId}-ROADMAP.json`,
	);
}

function roadmapMdPath(basePath, milestoneId) {
	return join(milestoneDir(basePath, milestoneId), `${milestoneId}-ROADMAP.md`);
}

function projectDbPath(basePath) {
	return join(basePath, ".sf", "sf.db");
}

function okResult(source, milestone, slices, paths) {
	return {
		safe: true,
		unsafe: false,
		blocked: false,
		source,
		milestoneId: milestone.id,
		milestone,
		slices,
		paths,
	};
}

function blockedResult(source, milestoneId, reason, paths) {
	return {
		safe: false,
		unsafe: true,
		blocked: true,
		source,
		milestoneId,
		reason,
		milestone: null,
		slices: [],
		paths,
	};
}

function dbExists(basePath) {
	return existsSync(projectDbPath(basePath));
}

function normalizeStringArray(value) {
	if (!Array.isArray(value)) return [];
	return value.filter((item) => typeof item === "string");
}

function normalizeMilestoneFromDb(row) {
	return {
		id: row.id,
		title: row.title ?? "",
		status: row.status ?? "",
		vision: row.vision ?? "",
		dependsOn: normalizeStringArray(row.depends_on),
		successCriteria: normalizeStringArray(row.success_criteria),
		definitionOfDone: normalizeStringArray(row.definition_of_done),
		requirementCoverage: row.requirement_coverage ?? "",
		boundaryMapMarkdown: row.boundary_map_markdown ?? "",
	};
}

function normalizeSliceFromDb(row) {
	return {
		id: row.id,
		title: row.title ?? "",
		status: row.status ?? "",
		risk: row.risk ?? "",
		depends: normalizeStringArray(row.depends),
		demo: row.demo ?? "",
		goal: row.goal ?? "",
		successCriteria: row.success_criteria ?? "",
		proofLevel: row.proof_level ?? "",
		integrationClosure: row.integration_closure ?? "",
		observabilityImpact: row.observability_impact ?? "",
		isSketch: row.is_sketch === 1,
		sketchScope: row.sketch_scope ?? "",
	};
}

function normalizeMilestoneFromProjection(raw, milestoneId) {
	const source =
		raw?.milestone && typeof raw.milestone === "object" ? raw.milestone : raw;
	return {
		id: String(source?.id ?? source?.milestoneId ?? milestoneId),
		title: String(source?.title ?? ""),
		status: String(source?.status ?? ""),
		vision: String(source?.vision ?? ""),
		dependsOn: normalizeStringArray(source?.dependsOn ?? source?.depends_on),
		successCriteria: normalizeStringArray(
			source?.successCriteria ?? source?.success_criteria,
		),
		definitionOfDone: normalizeStringArray(
			source?.definitionOfDone ?? source?.definition_of_done,
		),
		requirementCoverage: String(
			source?.requirementCoverage ?? source?.requirement_coverage ?? "",
		),
		boundaryMapMarkdown: String(
			source?.boundaryMapMarkdown ?? source?.boundary_map_markdown ?? "",
		),
	};
}

function normalizeSliceFromProjection(raw) {
	return {
		id: String(raw?.id ?? raw?.sliceId ?? ""),
		title: String(raw?.title ?? ""),
		status: String(raw?.status ?? ""),
		risk: String(raw?.risk ?? ""),
		depends: normalizeStringArray(raw?.depends),
		demo: String(raw?.demo ?? ""),
		goal: String(raw?.goal ?? ""),
		successCriteria: String(
			raw?.successCriteria ?? raw?.success_criteria ?? "",
		),
		proofLevel: String(raw?.proofLevel ?? raw?.proof_level ?? ""),
		integrationClosure: String(
			raw?.integrationClosure ?? raw?.integration_closure ?? "",
		),
		observabilityImpact: String(
			raw?.observabilityImpact ?? raw?.observability_impact ?? "",
		),
		isSketch: raw?.isSketch === true || raw?.is_sketch === 1,
		sketchScope: String(raw?.sketchScope ?? raw?.sketch_scope ?? ""),
		sequence: Number(raw?.sequence ?? 0),
	};
}

function readDbPlan(basePath, milestoneId) {
	const dbPath = projectDbPath(basePath);
	const activeDbPath = getDbPath();
	if (!isDbAvailable() && existsSync(dbPath)) {
		openDatabase(dbPath);
	} else if (
		isDbAvailable() &&
		activeDbPath &&
		activeDbPath !== dbPath &&
		existsSync(dbPath)
	) {
		openDatabase(dbPath);
	}
	const openedDbPath = getDbPath();
	if (
		isDbAvailable() &&
		openedDbPath &&
		openedDbPath !== dbPath &&
		!existsSync(dbPath)
	) {
		return null;
	}
	if (!isDbAvailable()) return null;
	return readTransaction(() => {
		const milestone = getMilestone(milestoneId);
		if (!milestone) {
			return { missing: "milestone" };
		}
		const slices = getMilestoneSlices(milestoneId);
		if (slices.length === 0) {
			return {
				missing: "slices",
				milestone: normalizeMilestoneFromDb(milestone),
			};
		}
		return {
			milestone: normalizeMilestoneFromDb(milestone),
			slices: slices.map(normalizeSliceFromDb),
		};
	});
}

function readProjectionPlan(basePath, milestoneId) {
	const path = roadmapJsonPath(basePath, milestoneId);
	if (!existsSync(path)) return null;
	const raw = JSON.parse(readFileSync(path, "utf8"));
	const rawSlices = Array.isArray(raw?.slices)
		? raw.slices
		: Array.isArray(raw?.milestone?.slices)
			? raw.milestone.slices
			: [];
	const slices = rawSlices
		.map(normalizeSliceFromProjection)
		.filter((slice) => slice.id);
	if (slices.length === 0) {
		throw new Error(`${milestoneId}-ROADMAP.json has no slices`);
	}
	return {
		milestone: normalizeMilestoneFromProjection(raw, milestoneId),
		slices,
	};
}

/**
 * Return the canonical milestone plan for dispatch decisions.
 *
 * Purpose: prefer structured state over rendered Markdown so stale ROADMAP.md
 * rows cannot enqueue work. Consumers should treat `safe:false` as a stop.
 *
 * Consumer: auto dispatch and doctor migration flows that need milestone
 * slices without parsing ROADMAP.md as executable state.
 */
export function getCanonicalMilestonePlan(basePath, milestoneId, options = {}) {
	const paths = {
		db: projectDbPath(basePath),
		projection: roadmapJsonPath(basePath, milestoneId),
		markdown: roadmapMdPath(basePath, milestoneId),
	};
	const allowProjectionFallback = options.allowProjectionFallback === true;
	try {
		const dbPlan = readDbPlan(basePath, milestoneId);
		if (dbPlan?.missing) {
			return blockedResult(
				`db-missing-${dbPlan.missing}`,
				milestoneId,
				`.sf/sf.db is available, so ${milestoneId} must be read from DB rows. Missing ${dbPlan.missing}; projection files are export/recovery only. Run /doctor or sf recover to reconcile.`,
				paths,
			);
		}
		if (dbPlan) return okResult("db", dbPlan.milestone, dbPlan.slices, paths);
	} catch (err) {
		if (dbExists(basePath)) {
			return blockedResult(
				"db-unavailable",
				milestoneId,
				`.sf/sf.db exists but could not be read: ${getErrorMessage(err)}. Runtime does not fall back to projections when DB is expected; run sf recover.`,
				paths,
			);
		}
	}
	if (dbExists(basePath) && !allowProjectionFallback) {
		return blockedResult(
			"db-required",
			milestoneId,
			`.sf/sf.db exists, so projection files are not executable runtime state. Populate DB rows or run sf recover.`,
			paths,
		);
	}
	try {
		const projectionPlan = readProjectionPlan(basePath, milestoneId);
		if (projectionPlan) {
			return okResult(
				"projection",
				projectionPlan.milestone,
				projectionPlan.slices,
				paths,
			);
		}
	} catch (err) {
		return blockedResult(
			"projection-invalid",
			milestoneId,
			getErrorMessage(err),
			paths,
		);
	}
	if (existsSync(paths.markdown)) {
		return blockedResult(
			"markdown-only",
			milestoneId,
			`${milestoneId}-ROADMAP.md is rendered display state only; create ${milestoneId}-ROADMAP.json or populate .sf/sf.db before dispatch.`,
			paths,
		);
	}
	return blockedResult(
		"missing",
		milestoneId,
		`No canonical plan found for ${milestoneId}; expected populated DB rows or ${milestoneId}-ROADMAP.json.`,
		paths,
	);
}
