import { clearParseCache } from "../files.js";
import { renderRoadmapFromDb } from "../markdown-renderer.js";
import {
	hasStructuredVisionAlignmentMeeting,
	type VisionAlignmentMeetingRecord,
} from "../milestone-quality.js";
import {
	getMilestone,
	getMilestoneSlices,
	getSlice,
	insertMilestone,
	insertSlice,
	transaction,
	upsertMilestonePlanning,
	upsertSlicePlanning,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { isClosedStatus } from "../status-guards.js";
import {
	isNonEmptyString,
	normalizePlanningText,
	normalizePlanningTextArray,
} from "../validation.js";
import { appendEvent } from "../workflow-events.js";
import { logWarning } from "../workflow-logger.js";
import { writeManifest } from "../workflow-manifest.js";
import { renderAllProjections } from "../workflow-projections.js";
import { scaffoldMilestoneSlices } from "../workflow-templates.js";

export interface PlanMilestoneSliceInput {
	sliceId: string;
	title: string;
	risk: string;
	depends: string[];
	demo: string;
	goal: string;
	successCriteria: string;
	proofLevel: string;
	integrationClosure: string;
	observabilityImpact: string;
}

export interface PlanMilestoneParams {
	milestoneId: string;
	title: string;
	vision: string;
	slices?: PlanMilestoneSliceInput[];
	templateId?: string;
	status?: string;
	dependsOn?: string[];
	/** Optional caller-provided identity for audit trail */
	actorName?: string;
	/** Optional caller-provided reason this action was triggered */
	triggerReason?: string;
	/** @optional — defaults to [] when omitted by models with limited tool-calling */
	successCriteria?: string[];
	/** @optional — defaults to [] when omitted */
	keyRisks?: Array<{ risk: string; whyItMatters: string }>;
	/** @optional — defaults to [] when omitted */
	proofStrategy?: Array<{
		riskOrUnknown: string;
		retireIn: string;
		whatWillBeProven: string;
	}>;
	/** @optional — defaults to "" when omitted */
	verificationContract?: string;
	/** @optional — defaults to "" when omitted */
	verificationIntegration?: string;
	/** @optional — defaults to "" when omitted */
	verificationOperational?: string;
	/** @optional — defaults to "" when omitted */
	verificationUat?: string;
	/** @optional — defaults to [] when omitted */
	definitionOfDone?: string[];
	/** @optional — defaults to "Not provided." when omitted */
	requirementCoverage?: string;
	/** @optional — defaults to "Not provided." when omitted */
	boundaryMapMarkdown?: string;
	/** Optional structured top-level vision/roadmap alignment meeting. */
	visionMeeting?: VisionAlignmentMeetingRecord;
}

export interface PlanMilestoneResult {
	milestoneId: string;
	title: string;
	sliceCount: number;
	firstSliceId?: string;
	firstSliceTitle?: string;
	roadmapPath: string;
}

function validateRiskEntries(
	value: unknown,
): Array<{ risk: string; whyItMatters: string }> {
	if (!Array.isArray(value)) {
		throw new Error("keyRisks must be an array");
	}
	return value.map((entry, index) => {
		if (!entry || typeof entry !== "object") {
			throw new Error(`keyRisks[${index}] must be an object`);
		}
		const risk = (entry as Record<string, unknown>).risk;
		const whyItMatters = (entry as Record<string, unknown>).whyItMatters;
		if (!isNonEmptyString(risk) || !isNonEmptyString(whyItMatters)) {
			throw new Error(
				`keyRisks[${index}] must include non-empty risk and whyItMatters`,
			);
		}
		return {
			risk: normalizePlanningText(risk, `keyRisks[${index}].risk`),
			whyItMatters: normalizePlanningText(
				whyItMatters,
				`keyRisks[${index}].whyItMatters`,
			),
		};
	});
}

function validateProofStrategy(value: unknown): Array<{
	riskOrUnknown: string;
	retireIn: string;
	whatWillBeProven: string;
}> {
	if (!Array.isArray(value)) {
		throw new Error("proofStrategy must be an array");
	}
	return value.map((entry, index) => {
		if (!entry || typeof entry !== "object") {
			throw new Error(`proofStrategy[${index}] must be an object`);
		}
		const riskOrUnknown = (entry as Record<string, unknown>).riskOrUnknown;
		const retireIn = (entry as Record<string, unknown>).retireIn;
		const whatWillBeProven = (entry as Record<string, unknown>)
			.whatWillBeProven;
		if (
			!isNonEmptyString(riskOrUnknown) ||
			!isNonEmptyString(retireIn) ||
			!isNonEmptyString(whatWillBeProven)
		) {
			throw new Error(
				`proofStrategy[${index}] must include non-empty riskOrUnknown, retireIn, and whatWillBeProven`,
			);
		}
		return {
			riskOrUnknown: normalizePlanningText(
				riskOrUnknown,
				`proofStrategy[${index}].riskOrUnknown`,
			),
			retireIn: normalizePlanningText(
				retireIn,
				`proofStrategy[${index}].retireIn`,
			),
			whatWillBeProven: normalizePlanningText(
				whatWillBeProven,
				`proofStrategy[${index}].whatWillBeProven`,
			),
		};
	});
}

function validateSlices(value: unknown): PlanMilestoneSliceInput[] {
	if (!Array.isArray(value) || value.length === 0) {
		throw new Error("slices must be a non-empty array");
	}

	const seen = new Set<string>();
	return value.map((entry, index) => {
		if (!entry || typeof entry !== "object") {
			throw new Error(`slices[${index}] must be an object`);
		}
		const obj = entry as Record<string, unknown>;
		const sliceId = obj.sliceId;
		const title = obj.title;
		const risk = obj.risk;
		const depends = obj.depends;
		const demo = obj.demo;
		const goal = obj.goal;
		const successCriteria = obj.successCriteria;
		const proofLevel = obj.proofLevel;
		const integrationClosure = obj.integrationClosure;
		const observabilityImpact = obj.observabilityImpact;

		if (!isNonEmptyString(sliceId))
			throw new Error(`slices[${index}].sliceId must be a non-empty string`);
		if (seen.has(sliceId))
			throw new Error(`slices[${index}].sliceId must be unique`);
		seen.add(sliceId);
		if (!isNonEmptyString(title))
			throw new Error(`slices[${index}].title must be a non-empty string`);
		if (!isNonEmptyString(risk))
			throw new Error(`slices[${index}].risk must be a non-empty string`);
		if (
			!Array.isArray(depends) ||
			depends.some((item) => !isNonEmptyString(item))
		) {
			throw new Error(
				`slices[${index}].depends must be an array of non-empty strings`,
			);
		}
		if (!isNonEmptyString(demo))
			throw new Error(`slices[${index}].demo must be a non-empty string`);
		if (!isNonEmptyString(goal))
			throw new Error(`slices[${index}].goal must be a non-empty string`);
		if (!isNonEmptyString(successCriteria))
			throw new Error(
				`slices[${index}].successCriteria must be a non-empty string`,
			);
		if (!isNonEmptyString(proofLevel))
			throw new Error(`slices[${index}].proofLevel must be a non-empty string`);
		if (!isNonEmptyString(integrationClosure))
			throw new Error(
				`slices[${index}].integrationClosure must be a non-empty string`,
			);
		if (!isNonEmptyString(observabilityImpact))
			throw new Error(
				`slices[${index}].observabilityImpact must be a non-empty string`,
			);

		return {
			sliceId: normalizePlanningText(sliceId, `slices[${index}].sliceId`),
			title: normalizePlanningText(title, `slices[${index}].title`),
			risk: normalizePlanningText(risk, `slices[${index}].risk`),
			depends: depends.map((item, depIndex) =>
				normalizePlanningText(item, `slices[${index}].depends[${depIndex}]`),
			),
			demo: normalizePlanningText(demo, `slices[${index}].demo`),
			goal: normalizePlanningText(goal, `slices[${index}].goal`),
			successCriteria: normalizePlanningText(
				successCriteria,
				`slices[${index}].successCriteria`,
			),
			proofLevel: normalizePlanningText(
				proofLevel,
				`slices[${index}].proofLevel`,
			),
			integrationClosure: normalizePlanningText(
				integrationClosure,
				`slices[${index}].integrationClosure`,
			),
			observabilityImpact: normalizePlanningText(
				observabilityImpact,
				`slices[${index}].observabilityImpact`,
			),
		};
	});
}

function normalizeVisionMeeting(
	meeting: VisionAlignmentMeetingRecord,
): VisionAlignmentMeetingRecord {
	return {
		trigger: normalizePlanningText(meeting.trigger, "visionMeeting.trigger"),
		pm: normalizePlanningText(meeting.pm, "visionMeeting.pm"),
		userAdvocate: normalizePlanningText(
			meeting.userAdvocate,
			"visionMeeting.userAdvocate",
		),
		customerPanel: normalizePlanningText(
			meeting.customerPanel,
			"visionMeeting.customerPanel",
		),
		business: normalizePlanningText(meeting.business, "visionMeeting.business"),
		researcher: normalizePlanningText(
			meeting.researcher,
			"visionMeeting.researcher",
		),
		deliveryLead: normalizePlanningText(
			meeting.deliveryLead,
			"visionMeeting.deliveryLead",
		),
		partner: normalizePlanningText(meeting.partner, "visionMeeting.partner"),
		combatant: normalizePlanningText(
			meeting.combatant,
			"visionMeeting.combatant",
		),
		architect: normalizePlanningText(
			meeting.architect,
			"visionMeeting.architect",
		),
		moderator: normalizePlanningText(
			meeting.moderator,
			"visionMeeting.moderator",
		),
		weightedSynthesis: normalizePlanningText(
			meeting.weightedSynthesis,
			"visionMeeting.weightedSynthesis",
		),
		confidenceByArea: normalizePlanningText(
			meeting.confidenceByArea,
			"visionMeeting.confidenceByArea",
		),
		recommendedRoute: meeting.recommendedRoute,
	};
}

function validateParams(params: PlanMilestoneParams): PlanMilestoneParams {
	if (!isNonEmptyString(params?.milestoneId))
		throw new Error("milestoneId is required");
	if (!isNonEmptyString(params?.title)) throw new Error("title is required");
	if (!isNonEmptyString(params?.vision)) throw new Error("vision is required");

	let slicesInput = params.slices;
	if (
		(!Array.isArray(slicesInput) || slicesInput.length === 0) &&
		params.templateId
	) {
		const scaffolded = scaffoldMilestoneSlices(params.templateId);
		if (!scaffolded) {
			throw new Error(`unknown milestone template: ${params.templateId}`);
		}
		slicesInput = scaffolded;
	}

	return {
		...params,
		milestoneId: normalizePlanningText(params.milestoneId, "milestoneId"),
		title: normalizePlanningText(params.title, "title"),
		vision: normalizePlanningText(params.vision, "vision"),
		dependsOn: params.dependsOn
			? normalizePlanningTextArray(params.dependsOn, "dependsOn")
			: [],
		// Apply defaults for optional enrichment fields (#2771)
		successCriteria: params.successCriteria
			? normalizePlanningTextArray(params.successCriteria, "successCriteria")
			: [],
		keyRisks: params.keyRisks ? validateRiskEntries(params.keyRisks) : [],
		proofStrategy: params.proofStrategy
			? validateProofStrategy(params.proofStrategy)
			: [],
		verificationContract: normalizePlanningText(
			params.verificationContract ?? "",
			"verificationContract",
		),
		verificationIntegration: normalizePlanningText(
			params.verificationIntegration ?? "",
			"verificationIntegration",
		),
		verificationOperational: normalizePlanningText(
			params.verificationOperational ?? "",
			"verificationOperational",
		),
		verificationUat: normalizePlanningText(
			params.verificationUat ?? "",
			"verificationUat",
		),
		definitionOfDone: params.definitionOfDone
			? normalizePlanningTextArray(params.definitionOfDone, "definitionOfDone")
			: [],
		requirementCoverage: normalizePlanningText(
			params.requirementCoverage ?? "Not provided.",
			"requirementCoverage",
		),
		boundaryMapMarkdown: normalizePlanningText(
			params.boundaryMapMarkdown ?? "Not provided.",
			"boundaryMapMarkdown",
		),
		visionMeeting: hasStructuredVisionAlignmentMeeting(params.visionMeeting)
			? normalizeVisionMeeting(params.visionMeeting)
			: undefined,
		slices: validateSlices(slicesInput),
	};
}

export async function handlePlanMilestone(
	rawParams: PlanMilestoneParams,
	basePath: string,
): Promise<PlanMilestoneResult | { error: string }> {
	let params: PlanMilestoneParams;
	try {
		params = validateParams(rawParams);
	} catch (err) {
		return { error: `validation failed: ${(err as Error).message}` };
	}
	const slices = params.slices ?? [];

	// ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
	// Guards must be inside the transaction so the state they check cannot
	// change between the read and the write (#2723).
	let guardError: string | null = null;

	try {
		transaction(() => {
			const existingMilestone = getMilestone(params.milestoneId);
			if (existingMilestone && isClosedStatus(existingMilestone.status)) {
				guardError = `cannot re-plan milestone ${params.milestoneId}: it is already complete`;
				return;
			}

			// Guard: refuse to re-plan a milestone that would drop completed slices (#2960).
			// Allow re-planning when all completed slices are still present in the
			// incoming plan — their status is preserved below (#2558). Block only when
			// the new plan omits a completed slice, which could shadow completed work.
			const existingSlices = getMilestoneSlices(params.milestoneId);
			const completedSlices = existingSlices.filter((s) =>
				isClosedStatus(s.status),
			);
			if (completedSlices.length > 0) {
				const incomingSliceIds = new Set(slices.map((s) => s.sliceId));
				const droppedCompleted = completedSlices.filter(
					(s) => !incomingSliceIds.has(s.id),
				);
				if (droppedCompleted.length > 0) {
					guardError = `cannot re-plan milestone ${params.milestoneId}: ${droppedCompleted.length} completed slice(s) would be dropped (${droppedCompleted.map((s) => s.id).join(", ")}). Use sf_reassess_roadmap to modify the roadmap.`;
					return;
				}
			}

			// Validate depends_on: all dependencies must exist and be complete
			if (params.dependsOn && params.dependsOn.length > 0) {
				for (const depId of params.dependsOn) {
					const dep = getMilestone(depId);
					if (!dep) {
						guardError = `depends_on references unknown milestone: ${depId}`;
						return;
					}
					if (!isClosedStatus(dep.status)) {
						guardError = `depends_on milestone ${depId} is not yet complete (status: ${dep.status})`;
						return;
					}
				}
			}

			insertMilestone({
				id: params.milestoneId,
				title: params.title,
				status: params.status ?? "active",
				depends_on: params.dependsOn ?? [],
			});

			upsertMilestonePlanning(params.milestoneId, {
				title: params.title,
				status: params.status ?? "active",
				vision: params.vision,
				successCriteria: params.successCriteria,
				keyRisks: params.keyRisks,
				proofStrategy: params.proofStrategy,
				verificationContract: params.verificationContract,
				verificationIntegration: params.verificationIntegration,
				verificationOperational: params.verificationOperational,
				verificationUat: params.verificationUat,
				definitionOfDone: params.definitionOfDone,
				requirementCoverage: params.requirementCoverage,
				boundaryMapMarkdown: params.boundaryMapMarkdown,
				visionMeeting: params.visionMeeting,
			});

			for (let i = 0; i < slices.length; i++) {
				const slice = slices[i]!;
				// Preserve completed/done status on re-plan (#2558).
				// Without this, a re-plan after milestone transition would reset
				// already-completed slices back to "pending".
				const existing = getSlice(params.milestoneId, slice.sliceId);
				const status =
					existing &&
					(existing.status === "complete" || existing.status === "done")
						? existing.status
						: "pending";
				insertSlice({
					id: slice.sliceId,
					milestoneId: params.milestoneId,
					title: slice.title,
					status,
					risk: slice.risk,
					depends: slice.depends,
					demo: slice.demo,
					sequence: i + 1, // Preserve agent-ordered sequence (#3356)
				});
				upsertSlicePlanning(params.milestoneId, slice.sliceId, {
					goal: slice.goal,
					successCriteria: slice.successCriteria,
					proofLevel: slice.proofLevel,
					integrationClosure: slice.integrationClosure,
					observabilityImpact: slice.observabilityImpact,
				});
			}
		});
	} catch (err) {
		return { error: `db write failed: ${(err as Error).message}` };
	}

	if (guardError) {
		return { error: guardError };
	}

	let roadmapPath: string;
	try {
		const renderResult = await renderRoadmapFromDb(
			basePath,
			params.milestoneId,
		);
		roadmapPath = renderResult.roadmapPath;
	} catch (renderErr) {
		logWarning(
			"tool",
			`plan_milestone — render failed (DB rows preserved for debugging): ${(renderErr as Error).message}`,
		);
		invalidateStateCache();
		return { error: `render failed: ${(renderErr as Error).message}` };
	}

	invalidateStateCache();
	clearParseCache();

	// ── Post-mutation hook: projections, manifest, event log ───────────────
	try {
		await renderAllProjections(basePath, params.milestoneId);
		writeManifest(basePath);
		appendEvent(basePath, {
			cmd: "plan-milestone",
			params: { milestoneId: params.milestoneId },
			ts: new Date().toISOString(),
			actor: "agent",
			actor_name: params.actorName,
			trigger_reason: params.triggerReason,
		});
	} catch (hookErr) {
		logWarning(
			"tool",
			`plan-milestone post-mutation hook warning: ${(hookErr as Error).message}`,
		);
	}

	return {
		milestoneId: params.milestoneId,
		title: params.title,
		sliceCount: params.slices?.length ?? 0,
		firstSliceId: params.slices?.[0]?.sliceId,
		firstSliceTitle: params.slices?.[0]?.title,
		roadmapPath,
	};
}
