import { clearParseCache } from "../files.js";
import { renderTaskPlanFromDb } from "../markdown-renderer.js";
import {
	getSlice,
	getTask,
	insertTask,
	insertTaskEvidence,
	transaction,
	upsertTaskPlanning,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { isClosedStatus } from "../status-guards.js";
import {
	isNonEmptyString,
	normalizePlanningText,
	normalizePlanningTextArray,
} from "../validation.js";
import { logWarning } from "../workflow-logger.js";
import { runPostMutationHook } from "./tool-helpers.js";

function validateParams(params) {
	if (!isNonEmptyString(params?.milestoneId))
		throw new Error("milestoneId is required");
	if (!isNonEmptyString(params?.sliceId))
		throw new Error("sliceId is required");
	if (!isNonEmptyString(params?.taskId)) throw new Error("taskId is required");
	if (!isNonEmptyString(params?.title)) throw new Error("title is required");
	if (!isNonEmptyString(params?.description))
		throw new Error("description is required");
	if (!isNonEmptyString(params?.estimate))
		throw new Error("estimate is required");
	if (!isNonEmptyString(params?.verify)) throw new Error("verify is required");
	if (
		params.observabilityImpact !== undefined &&
		!isNonEmptyString(params.observabilityImpact)
	) {
		throw new Error(
			"observabilityImpact must be a non-empty string when provided",
		);
	}
	return {
		...params,
		milestoneId: normalizePlanningText(params.milestoneId, "milestoneId"),
		sliceId: normalizePlanningText(params.sliceId, "sliceId"),
		taskId: normalizePlanningText(params.taskId, "taskId"),
		title: normalizePlanningText(params.title, "title"),
		description: normalizePlanningText(params.description, "description"),
		estimate: normalizePlanningText(params.estimate, "estimate"),
		verify: normalizePlanningText(params.verify, "verify"),
		files: normalizePlanningTextArray(params.files, "files"),
		inputs: normalizePlanningTextArray(params.inputs, "inputs"),
		expectedOutput: normalizePlanningTextArray(
			params.expectedOutput,
			"expectedOutput",
		),
		observabilityImpact:
			params.observabilityImpact === undefined
				? undefined
				: normalizePlanningText(
						params.observabilityImpact,
						"observabilityImpact",
					),
	};
}
export async function handlePlanTask(rawParams, basePath) {
	let params;
	try {
		params = validateParams(rawParams);
	} catch (err) {
		return { error: `validation failed: ${err.message}` };
	}
	// ── 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 = null;
	try {
		transaction(() => {
			const parentSlice = getSlice(params.milestoneId, params.sliceId);
			if (!parentSlice) {
				guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
				return;
			}
			if (isClosedStatus(parentSlice.status)) {
				guardError = `cannot plan task in a closed slice: ${params.sliceId} (status: ${parentSlice.status})`;
				return;
			}
			const existingTask = getTask(
				params.milestoneId,
				params.sliceId,
				params.taskId,
			);
			if (existingTask && isClosedStatus(existingTask.status)) {
				guardError = `cannot re-plan task ${params.taskId}: it is already complete — use sf_task_reopen first`;
				return;
			}
			if (!existingTask) {
				insertTask({
					id: params.taskId,
					sliceId: params.sliceId,
					milestoneId: params.milestoneId,
					title: params.title,
					status: "pending",
				});
			}
			upsertTaskPlanning(params.milestoneId, params.sliceId, params.taskId, {
				title: params.title,
				description: params.description,
				estimate: params.estimate,
				files: params.files,
				verify: params.verify,
				inputs: params.inputs,
				expectedOutput: params.expectedOutput,
				observabilityImpact: params.observabilityImpact ?? "",
				fullPlanMd: params.fullPlanMd,
			});
			insertTaskEvidence(
				params.milestoneId,
				params.sliceId,
				params.taskId,
				"plan_task",
				JSON.stringify({
					title: params.title,
					description: params.description,
					estimate: params.estimate ?? "",
					files: params.files ?? [],
					verify: params.verify ?? "",
					inputs: params.inputs ?? [],
					expectedOutput: params.expectedOutput ?? [],
					observabilityImpact: params.observabilityImpact ?? "",
				}),
				"plan-task",
				"agent",
			);
		});
	} catch (err) {
		return { error: `db write failed: ${err.message}` };
	}
	if (guardError) {
		return { error: guardError };
	}
	try {
		const renderResult = await renderTaskPlanFromDb(
			basePath,
			params.milestoneId,
			params.sliceId,
			params.taskId,
		);
		invalidateStateCache();
		clearParseCache();
		// ── Post-mutation hook: projections, manifest, event log ─────────────
		await runPostMutationHook(
			basePath,
			params.milestoneId,
			{
				cmd: "plan-task",
				params: {
					milestoneId: params.milestoneId,
					sliceId: params.sliceId,
					taskId: params.taskId,
				},
				ts: new Date().toISOString(),
				actor: "agent",
				actor_name: params.actorName,
				trigger_reason: params.triggerReason,
			},
			{ caller: "plan-task" },
		);
		return {
			milestoneId: params.milestoneId,
			sliceId: params.sliceId,
			taskId: params.taskId,
			taskPlanPath: renderResult.taskPlanPath,
		};
	} catch (err) {
		return { error: `render failed: ${err.message}` };
	}
}
