import { clearParseCache } from "../files.js";
import { renderPlanFromDb } from "../markdown-renderer.js";
import {
	hasCompleteAdversarialReview,
	hasStructuredPlanningMeeting,
} from "../plan-quality.js";
import {
	clearSliceSketch,
	getMilestone,
	getSlice,
	insertGateRow,
	insertSliceEvidence,
	insertTask,
	transaction,
	upsertSlicePlanning,
	upsertTaskPlanning,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { isClosedStatus } from "../status-guards.js";
import { taskFrontmatterFromRecord } from "../task-frontmatter.js";
import { isNonEmptyString, normalizePlanningText } from "../validation.js";
import { logWarning } from "../workflow-logger.js";
import { runPostMutationHook } from "./tool-helpers.js";

const PLANNING_MEETING_REQUIRED_MESSAGE =
	"planningMeeting must be a populated object — write at least 2-3 perspectives. Skipping is not allowed.";
function validateTasks(value) {
	if (!Array.isArray(value) || value.length === 0) {
		throw new Error("tasks must be a non-empty array");
	}
	const seen = new Set();
	return value.map((entry, index) => {
		if (!entry || typeof entry !== "object") {
			throw new Error(`tasks[${index}] must be an object`);
		}
		const obj = entry;
		const taskId = obj.taskId;
		const title = obj.title;
		const description = obj.description;
		const estimate = obj.estimate;
		const files = obj.files;
		const verify = obj.verify;
		const inputs = obj.inputs;
		const expectedOutput = obj.expectedOutput;
		const observabilityImpact = obj.observabilityImpact;
		if (!isNonEmptyString(taskId))
			throw new Error(`tasks[${index}].taskId must be a non-empty string`);
		if (seen.has(taskId))
			throw new Error(`tasks[${index}].taskId must be unique`);
		seen.add(taskId);
		if (!isNonEmptyString(title))
			throw new Error(`tasks[${index}].title must be a non-empty string`);
		if (!isNonEmptyString(description))
			throw new Error(`tasks[${index}].description must be a non-empty string`);
		if (!isNonEmptyString(estimate))
			throw new Error(`tasks[${index}].estimate must be a non-empty string`);
		if (
			!Array.isArray(files) ||
			files.some((item) => !isNonEmptyString(item))
		) {
			throw new Error(
				`tasks[${index}].files must be an array of non-empty strings`,
			);
		}
		if (!isNonEmptyString(verify))
			throw new Error(`tasks[${index}].verify must be a non-empty string`);
		if (
			!Array.isArray(inputs) ||
			inputs.some((item) => !isNonEmptyString(item))
		) {
			throw new Error(
				`tasks[${index}].inputs must be an array of non-empty strings`,
			);
		}
		if (
			!Array.isArray(expectedOutput) ||
			expectedOutput.some((item) => !isNonEmptyString(item))
		) {
			throw new Error(
				`tasks[${index}].expectedOutput must be an array of non-empty strings`,
			);
		}
		if (
			observabilityImpact !== undefined &&
			!isNonEmptyString(observabilityImpact)
		) {
			throw new Error(
				`tasks[${index}].observabilityImpact must be a non-empty string when provided`,
			);
		}
		const frontmatter = taskFrontmatterFromRecord({
			...obj,
			keyFiles: files,
		});
		if (!frontmatter.valid) {
			throw new Error(
				`tasks[${index}] metadata invalid: ${frontmatter.errors.join("; ")}`,
			);
		}
		return {
			taskId: normalizePlanningText(taskId, `tasks[${index}].taskId`),
			title: normalizePlanningText(title, `tasks[${index}].title`),
			description: normalizePlanningText(
				description,
				`tasks[${index}].description`,
			),
			estimate: normalizePlanningText(estimate, `tasks[${index}].estimate`),
			files,
			verify: normalizePlanningText(verify, `tasks[${index}].verify`),
			inputs,
			expectedOutput,
			observabilityImpact:
				typeof observabilityImpact === "string"
					? normalizePlanningText(
							observabilityImpact,
							`tasks[${index}].observabilityImpact`,
						)
					: "",
			risk: frontmatter.normalized.risk,
			mutationScope: frontmatter.normalized.mutationScope,
			verification: frontmatter.normalized.verification,
			planApproval: frontmatter.normalized.planApproval,
			estimatedEffort: frontmatter.normalized.estimatedEffort,
			dependencies: frontmatter.normalized.dependencies,
			blocksParallel: frontmatter.normalized.blocksParallel,
			requiresUserInput: frontmatter.normalized.requiresUserInput,
			autoRetry: frontmatter.normalized.autoRetry,
			maxRetries: frontmatter.normalized.maxRetries,
		};
	});
}
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?.goal)) throw new Error("goal is required");
	const planningMeeting = params?.planningMeeting;
	if (!hasStructuredPlanningMeeting(planningMeeting)) {
		throw new Error(PLANNING_MEETING_REQUIRED_MESSAGE);
	}
	return {
		...params,
		// Apply defaults for optional enrichment fields (#2771)
		goal: normalizePlanningText(params.goal, "goal"),
		successCriteria: normalizePlanningText(
			params.successCriteria ?? "Not provided.",
			"successCriteria",
		),
		proofLevel: normalizePlanningText(
			params.proofLevel ?? "Not provided.",
			"proofLevel",
		),
		integrationClosure: normalizePlanningText(
			params.integrationClosure ?? "Not provided.",
			"integrationClosure",
		),
		observabilityImpact: normalizePlanningText(
			params.observabilityImpact ?? "Not provided.",
			"observabilityImpact",
		),
		adversarialReview: hasCompleteAdversarialReview(params.adversarialReview)
			? {
					partner: params.adversarialReview.partner.trim(),
					combatant: params.adversarialReview.combatant.trim(),
					architect: params.adversarialReview.architect.trim(),
				}
			: {
					partner: "Missing partner review.",
					combatant: "Missing combatant review.",
					architect: "Missing architect review.",
				},
		planningMeeting: {
			trigger: normalizePlanningText(
				planningMeeting.trigger,
				"planningMeeting.trigger",
			),
			pm: normalizePlanningText(planningMeeting.pm, "planningMeeting.pm"),
			...(isNonEmptyString(planningMeeting.userAdvocate)
				? {
						userAdvocate: normalizePlanningText(
							planningMeeting.userAdvocate,
							"planningMeeting.userAdvocate",
						),
					}
				: {}),
			...(isNonEmptyString(planningMeeting.customerPanel)
				? {
						customerPanel: normalizePlanningText(
							planningMeeting.customerPanel,
							"planningMeeting.customerPanel",
						),
					}
				: {}),
			...(isNonEmptyString(planningMeeting.business)
				? {
						business: normalizePlanningText(
							planningMeeting.business,
							"planningMeeting.business",
						),
					}
				: {}),
			researcher: normalizePlanningText(
				planningMeeting.researcher,
				"planningMeeting.researcher",
			),
			...(isNonEmptyString(planningMeeting.deliveryLead)
				? {
						deliveryLead: normalizePlanningText(
							planningMeeting.deliveryLead,
							"planningMeeting.deliveryLead",
						),
					}
				: {}),
			partner: normalizePlanningText(
				planningMeeting.partner,
				"planningMeeting.partner",
			),
			combatant: normalizePlanningText(
				planningMeeting.combatant,
				"planningMeeting.combatant",
			),
			architect: normalizePlanningText(
				planningMeeting.architect,
				"planningMeeting.architect",
			),
			moderator: normalizePlanningText(
				planningMeeting.moderator,
				"planningMeeting.moderator",
			),
			recommendedRoute: planningMeeting.recommendedRoute,
			confidenceSummary: normalizePlanningText(
				planningMeeting.confidenceSummary,
				"planningMeeting.confidenceSummary",
			),
		},
		tasks: validateTasks(params.tasks),
	};
}
export async function handlePlanSlice(rawParams, basePath, options = {}) {
	let params;
	try {
		params = validateParams(rawParams);
	} catch (err) {
		const message = err.message;
		return {
			error:
				message === PLANNING_MEETING_REQUIRED_MESSAGE
					? message
					: `validation failed: ${message}`,
		};
	}
	// Schema-v2 UOK run-context for the Q3/Q4/Q5/Q6/Q7/Q8 seed rows. When
	// the caller supplied a context (autonomous-loop dispatch chains it
	// through executePlanSlice), the gate rows land with surface/run_control/
	// permission_profile/trace_id populated and status-uok classifies them
	// as "ok". When absent (direct/legacy callers), the rows stay NULL and
	// classify as "legacy" — same shape as pre-v2.
	const uokContext =
		options && typeof options.uokContext === "object"
			? options.uokContext
			: null;
	// ── 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 parentMilestone = getMilestone(params.milestoneId);
			if (!parentMilestone) {
				guardError = `milestone not found: ${params.milestoneId}`;
				return;
			}
			if (isClosedStatus(parentMilestone.status)) {
				guardError = `cannot plan slice in a closed milestone: ${params.milestoneId} (status: ${parentMilestone.status})`;
				return;
			}
			const parentSlice = getSlice(params.milestoneId, params.sliceId);
			if (!parentSlice) {
				guardError = `missing parent slice: ${params.milestoneId}/${params.sliceId}`;
				return;
			}
			if (isClosedStatus(parentSlice.status)) {
				guardError = `cannot re-plan slice ${params.sliceId}: it is already complete — use sf_slice_reopen first`;
				return;
			}
			upsertSlicePlanning(params.milestoneId, params.sliceId, {
				goal: params.goal,
				successCriteria: params.successCriteria,
				proofLevel: params.proofLevel,
				integrationClosure: params.integrationClosure,
				observabilityImpact: params.observabilityImpact,
				adversarialReview: params.adversarialReview,
				planningMeeting: params.planningMeeting,
			});
			// SF ADR-011: when plan_slice runs against a sketch slice (refine-slice
			// produced a full plan from the sketch_scope hint), clear the is_sketch
			// flag atomically with the plan write so the next dispatch cycle no
			// longer routes to refine. Idempotent — no-op for non-sketches.
			if (parentSlice.is_sketch === 1) {
				clearSliceSketch(params.milestoneId, params.sliceId);
			}
			for (const task of params.tasks) {
				insertTask({
					id: task.taskId,
					sliceId: params.sliceId,
					milestoneId: params.milestoneId,
					title: task.title,
					status: "pending",
				});
				upsertTaskPlanning(params.milestoneId, params.sliceId, task.taskId, {
					title: task.title,
					description: task.description,
					estimate: task.estimate,
					files: task.files,
					verify: task.verify,
					inputs: task.inputs,
					expectedOutput: task.expectedOutput,
					observabilityImpact: task.observabilityImpact ?? "",
					fullPlanMd: task.fullPlanMd,
					risk: task.risk,
					mutationScope: task.mutationScope,
					verification: task.verification,
					planApproval: task.planApproval,
					estimatedEffort: task.estimatedEffort,
					dependencies: task.dependencies,
					blocksParallel: task.blocksParallel,
					requiresUserInput: task.requiresUserInput,
					autoRetry: task.autoRetry,
					maxRetries: task.maxRetries,
				});
			}
			// Seed quality gate rows inside the transaction — all-or-nothing with
			// the plan data so a crash can't leave orphaned gates without tasks.
			const sliceGates = ["Q3", "Q4"];
			for (const gid of sliceGates) {
				insertGateRow({
					milestoneId: params.milestoneId,
					sliceId: params.sliceId,
					gateId: gid,
					scope: "slice",
					uokContext,
				});
			}
			const taskGates = ["Q5", "Q6", "Q7"];
			for (const task of params.tasks) {
				for (const gid of taskGates) {
					insertGateRow({
						milestoneId: params.milestoneId,
						sliceId: params.sliceId,
						gateId: gid,
						scope: "task",
						taskId: task.taskId,
						uokContext,
					});
				}
			}
			insertGateRow({
				milestoneId: params.milestoneId,
				sliceId: params.sliceId,
				gateId: "Q8",
				scope: "slice",
				uokContext,
			});
			insertSliceEvidence(
				params.milestoneId,
				params.sliceId,
				"plan_slice",
				JSON.stringify({
					goal: params.goal ?? "",
					successCriteria: params.successCriteria ?? "",
					proofLevel: params.proofLevel ?? "",
					integrationClosure: params.integrationClosure ?? "",
					observabilityImpact: params.observabilityImpact ?? "",
					adversarialReview: params.adversarialReview ?? "",
					tasks: params.tasks.map((t) => ({
						taskId: t.taskId,
						title: t.title,
					})),
				}),
				"plan-slice",
				"agent",
			);
		});
	} catch (err) {
		return { error: `db write failed: ${err.message}` };
	}
	if (guardError) {
		return { error: guardError };
	}
	try {
		const renderResult = await renderPlanFromDb(
			basePath,
			params.milestoneId,
			params.sliceId,
		);
		invalidateStateCache();
		clearParseCache();
		// ── Post-mutation hook: projections, manifest, event log ─────────────
		await runPostMutationHook(
			basePath,
			params.milestoneId,
			{
				cmd: "plan-slice",
				params: { milestoneId: params.milestoneId, sliceId: params.sliceId },
				ts: new Date().toISOString(),
				actor: "agent",
				actor_name: params.actorName,
				trigger_reason: params.triggerReason,
			},
			{ caller: "plan-slice" },
		);
		return {
			milestoneId: params.milestoneId,
			sliceId: params.sliceId,
			planPath: renderResult.planPath,
			taskPlanPaths: renderResult.taskPlanPaths,
		};
	} catch (renderErr) {
		logWarning(
			"tool",
			`plan_slice — render failed (DB rows preserved for debugging): ${renderErr.message}`,
		);
		invalidateStateCache();
		return { error: `render failed: ${renderErr.message}` };
	}
}
