/**
 * complete-slice handler — the core operation behind complete_slice.
 *
 * Validates inputs, checks all tasks are complete, atomically renders
 * SUMMARY.md + UAT.md to disk, then writes the slice row to DB in a
 * transaction, toggles the roadmap checkbox, and invalidates caches.
 */
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { atomicWriteAsync } from "../atomic-write.js";
import { clearParseCache, extractUatType } from "../files.js";
import { getGatesForTurn } from "../gate-registry.js";
import { renderRoadmapCheckboxes } from "../markdown-renderer.js";
import { clearPathCache, resolveSlicePath } from "../paths.js";
import { checkSafeIds } from "../safety/safe-id.js";
import {
	getMilestone,
	getPendingGatesForTurn,
	getSlice,
	getSliceTasks,
	insertMilestone,
	insertSlice,
	insertSliceEvidence,
	saveGateResult,
	setSliceSummaryMd,
	transaction,
	updateSliceStatus,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { isClosedStatus } from "../status-guards.js";
import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
import {
	normalizePlanningText,
	normalizeRequiredPlanningText,
	validateSafePathSegment,
} from "../validation.js";
import { appendEvent } from "../workflow-events.js";
import { logError, logWarning } from "../workflow-logger.js";
import { writeManifest } from "../workflow-manifest.js";
import { renderAllProjections } from "../workflow-projections.js";
import { ensureWritableParent, errorMessage } from "./tool-helpers.js";

function yamlScalar(value) {
	if (/^[A-Za-z0-9_.-]+$/.test(value)) return value;
	return JSON.stringify(value);
}
function yamlList(values, empty) {
	return values.length > 0
		? values.map((value) => `  - ${yamlScalar(value)}`).join("\n")
		: empty;
}
function normalizeStringArray(value, field) {
	const items = Array.isArray(value)
		? value
		: typeof value === "string" && value.trim()
			? value
					.split(/\n/)
					.map((line) => line.replace(/^[\s\-*•]+/, "").trim())
					.filter(Boolean)
			: [];
	return items.map((item, index) =>
		normalizePlanningText(String(item), `${field}[${index}]`),
	);
}
function normalizeObjectArray(value, field, shape) {
	if (!Array.isArray(value)) return [];
	return value.map((item, index) => {
		if (!item || typeof item !== "object" || Array.isArray(item)) {
			throw new Error(`${field}[${index}] must be an object`);
		}
		const record = item;
		const next = {};
		for (const [key, label] of Object.entries(shape)) {
			next[key] = normalizePlanningText(
				String(record[String(key)] ?? ""),
				`${field}[${index}].${label}`,
			);
		}
		return next;
	});
}
function normalizeCompleteSliceParams(params) {
	return {
		...params,
		sliceId: validateSafePathSegment(params.sliceId, "sliceId"),
		milestoneId: validateSafePathSegment(params.milestoneId, "milestoneId"),
		sliceTitle: normalizeRequiredPlanningText(params.sliceTitle, "sliceTitle"),
		oneLiner: normalizeRequiredPlanningText(params.oneLiner, "oneLiner"),
		narrative: normalizeRequiredPlanningText(params.narrative, "narrative"),
		verification: normalizeRequiredPlanningText(
			params.verification,
			"verification",
		),
		uatContent: normalizeRequiredPlanningText(params.uatContent, "uatContent"),
		keyFiles: normalizeStringArray(params.keyFiles, "keyFiles"),
		keyDecisions: normalizeStringArray(params.keyDecisions, "keyDecisions"),
		patternsEstablished: normalizeStringArray(
			params.patternsEstablished,
			"patternsEstablished",
		),
		observabilitySurfaces: normalizeStringArray(
			params.observabilitySurfaces,
			"observabilitySurfaces",
		),
		provides: normalizeStringArray(params.provides, "provides"),
		affects: normalizeStringArray(params.affects, "affects"),
		drillDownPaths: normalizeStringArray(
			params.drillDownPaths,
			"drillDownPaths",
		),
		requirementsSurfaced: normalizeStringArray(
			params.requirementsSurfaced,
			"requirementsSurfaced",
		),
		deviations:
			params.deviations === undefined
				? undefined
				: normalizePlanningText(params.deviations, "deviations"),
		knownLimitations:
			params.knownLimitations === undefined
				? undefined
				: normalizePlanningText(params.knownLimitations, "knownLimitations"),
		followUps:
			params.followUps === undefined
				? undefined
				: normalizePlanningText(params.followUps, "followUps"),
		operationalReadiness:
			params.operationalReadiness === undefined
				? undefined
				: normalizePlanningText(
						params.operationalReadiness,
						"operationalReadiness",
					),
		requirementsAdvanced: normalizeObjectArray(
			params.requirementsAdvanced,
			"requirementsAdvanced",
			{ id: "id", how: "how" },
		),
		requirementsValidated: normalizeObjectArray(
			params.requirementsValidated,
			"requirementsValidated",
			{
				id: "id",
				proof: "proof",
			},
		),
		requirementsInvalidated: normalizeObjectArray(
			params.requirementsInvalidated,
			"requirementsInvalidated",
			{
				id: "id",
				what: "what",
			},
		),
		filesModified: normalizeObjectArray(params.filesModified, "filesModified", {
			path: "path",
			description: "description",
		}),
		requires: normalizeObjectArray(params.requires, "requires", {
			slice: "slice",
			provides: "provides",
		}),
	};
}
async function writeMarkdownBeforeDb(filePath, content, label) {
	try {
		await ensureWritableParent(filePath);
		await atomicWriteAsync(filePath, content);
	} catch (error) {
		throw new Error(
			`${label} write failed at ${filePath}: ${errorMessage(error)}`,
		);
	}
}
/**
 * Map a complete-slice-owned gate id to the CompleteSliceParams field
 * whose presence drives `pass` vs. `omitted`. Keep this in lockstep with
 * the gates declared in gate-registry.ts under ownerTurn "complete-slice".
 */
function sliceGateFieldForId(id, params) {
	switch (id) {
		case "Q8":
			return params.operationalReadiness;
		default:
			return undefined;
	}
}
/**
 * Render slice summary markdown matching the template format.
 * YAML frontmatter uses snake_case keys for parseSummary() compatibility.
 */
function renderSliceSummaryMarkdown(params) {
	const now = new Date().toISOString();
	// Apply defaults for optional enrichment arrays (#2771)
	const provides = params.provides ?? [];
	const requires = params.requires ?? [];
	const affects = params.affects ?? [];
	const keyFiles = params.keyFiles ?? [];
	const keyDecisions = params.keyDecisions ?? [];
	const patternsEstablished = params.patternsEstablished ?? [];
	const observabilitySurfaces = params.observabilitySurfaces ?? [];
	const drillDownPaths = params.drillDownPaths ?? [];
	const requirementsAdvanced = params.requirementsAdvanced ?? [];
	const requirementsValidated = params.requirementsValidated ?? [];
	const requirementsSurfaced = params.requirementsSurfaced ?? [];
	const requirementsInvalidated = params.requirementsInvalidated ?? [];
	const filesModified = params.filesModified ?? [];
	const providesYaml =
		provides.length > 0 ? yamlList(provides, "  - (none)") : "  - (none)";
	const requiresYaml =
		requires.length > 0
			? requires
					.map(
						(r) =>
							`  - slice: ${yamlScalar(r.slice)}\n    provides: ${yamlScalar(r.provides)}`,
					)
					.join("\n")
			: "  []";
	const affectsYaml = affects.length > 0 ? yamlList(affects, "  []") : "  []";
	const keyFilesYaml =
		keyFiles.length > 0 ? yamlList(keyFiles, "  - (none)") : "  - (none)";
	const keyDecisionsYaml =
		keyDecisions.length > 0
			? yamlList(keyDecisions, "  - (none)")
			: "  - (none)";
	const patternsYaml =
		patternsEstablished.length > 0
			? yamlList(patternsEstablished, "  - (none)")
			: "  - (none)";
	const observabilityYaml =
		observabilitySurfaces.length > 0
			? yamlList(observabilitySurfaces, "  - none")
			: "  - none";
	const drillDownYaml =
		drillDownPaths.length > 0 ? yamlList(drillDownPaths, "  []") : "  []";
	// Requirements sections
	const reqAdvanced =
		requirementsAdvanced.length > 0
			? requirementsAdvanced.map((r) => `- ${r.id} — ${r.how}`).join("\n")
			: "None.";
	const reqValidated =
		requirementsValidated.length > 0
			? requirementsValidated.map((r) => `- ${r.id} — ${r.proof}`).join("\n")
			: "None.";
	const reqSurfaced =
		requirementsSurfaced.length > 0
			? requirementsSurfaced.map((r) => `- ${r}`).join("\n")
			: "None.";
	const reqInvalidated =
		requirementsInvalidated.length > 0
			? requirementsInvalidated.map((r) => `- ${r.id} — ${r.what}`).join("\n")
			: "None.";
	// Files modified
	const filesMod =
		filesModified.length > 0
			? filesModified
					.map((f) => `- \`${f.path}\` — ${f.description}`)
					.join("\n")
			: "None.";
	return `---
id: ${params.sliceId}
parent: ${params.milestoneId}
milestone: ${params.milestoneId}
provides:
${providesYaml}
requires:
${requiresYaml}
affects:
${affectsYaml}
key_files:
${keyFilesYaml}
key_decisions:
${keyDecisionsYaml}
patterns_established:
${patternsYaml}
observability_surfaces:
${observabilityYaml}
drill_down_paths:
${drillDownYaml}
duration: ""
verification_result: passed
completed_at: ${now}
blocker_discovered: false
---

# ${params.sliceId}: ${params.sliceTitle}

**${params.oneLiner}**

## What Happened

${params.narrative}

## Verification

${params.verification}

## Requirements Advanced

${reqAdvanced}

## Requirements Validated

${reqValidated}

## New Requirements Surfaced

${reqSurfaced}

## Requirements Invalidated or Re-scoped

${reqInvalidated}

## Operational Readiness

${params.operationalReadiness?.trim() || "None."}

## Deviations

${params.deviations || "None."}

## Known Limitations

${params.knownLimitations || "None."}

## Follow-ups

${params.followUps || "None."}

## Files Created/Modified

${filesMod}
`;
}
/**
 * Render UAT markdown matching the template format.
 *
 * When `uatContent` already contains a parsable `## UAT Type` block (validated
 * via `extractUatType`), the handler does **not** inject a second section so
 * the agent-chosen mode is what downstream tools observe. Otherwise the
 * handler prepends canonical `artifact-driven` defaults before `uatContent`.
 *
 * Purpose: preserve single-source truth for run-uat / verdict parsers without
 * forcing every caller to duplicate boilerplate when `artifact-driven` applies.
 */
function renderUatMarkdown(params) {
	const now = new Date().toISOString();
	const uatBodyRaw =
		typeof params.uatContent === "string" ? params.uatContent : "";
	const injectDefaultType = extractUatType(uatBodyRaw.trim()) === undefined;
	const typePreface = injectDefaultType
		? `## UAT Type

- UAT mode: artifact-driven
- Why this mode is sufficient: automated build + test verification

`
		: "";
	return `# ${params.sliceId}: ${params.sliceTitle} — UAT

**Milestone:** ${params.milestoneId}
**Written:** ${now}

${typePreface}${uatBodyRaw}`;
}
/**
 * Handle the complete_slice operation end-to-end.
 *
 * 1. Validate required fields
 * 2. Verify all tasks are complete
 * 3. Validate and atomically render SUMMARY.md + UAT.md to disk
 * 4. Write DB in a transaction (milestone, slice upsert, status update)
 * 5. Toggle roadmap checkbox
 * 7. Invalidate caches
 */
export async function handleCompleteSlice(paramsInput, basePath) {
	// ── Path-traversal guard (validation-safe-id-path-segments) ───────────
	// Checked on raw input before normalizeCompleteSliceParams so the
	// structured error shape is preserved. Rejects any ID that could escape
	// .sf/ when interpolated into a path or used in a DB write.
	const idCheck = checkSafeIds({
		milestoneId: paramsInput.milestoneId,
		sliceId: paramsInput.sliceId,
	});
	if (idCheck) {
		return {
			error: "unsafe_id",
			field: idCheck.fieldName,
			reason: idCheck.reason,
		};
	}
	let params;
	try {
		params = normalizeCompleteSliceParams(paramsInput);
	} catch (error) {
		return { error: errorMessage(error) };
	}
	// ── Validate required fields ────────────────────────────────────────────
	if (
		!params.sliceId ||
		typeof params.sliceId !== "string" ||
		params.sliceId.trim() === ""
	) {
		return { error: "sliceId is required and must be a non-empty string" };
	}
	if (
		!params.milestoneId ||
		typeof params.milestoneId !== "string" ||
		params.milestoneId.trim() === ""
	) {
		return { error: "milestoneId is required and must be a non-empty string" };
	}
	// ── Ownership check (opt-in: only enforced when claim file exists) ──────
	const ownershipErr = checkOwnership(
		basePath,
		sliceUnitKey(params.milestoneId, params.sliceId),
		params.actorName,
	);
	if (ownershipErr) {
		return { error: ownershipErr };
	}
	// ── Verification content gate (#3580) ──────────────────────────────────
	// Reject completion when the provided verification/UAT clearly indicates
	// the slice is blocked or failed. Prevents prompt regressions from
	// silently advancing blocked slices.
	const BLOCKED_SIGNALS =
		/\b(status:\s*blocked|verification_result:\s*failed|slice is blocked|cannot complete|verification failed)\b/i;
	if (
		BLOCKED_SIGNALS.test(params.verification || "") ||
		BLOCKED_SIGNALS.test(params.uatContent || "")
	) {
		return {
			error: `slice verification indicates blocked/failed state — do not complete a slice that has not passed verification. Address the blockers and re-verify first.`,
		};
	}
	// ── Guards before filesystem work ──────────────────────────────────────
	const completedAt = new Date().toISOString();
	const milestone = getMilestone(params.milestoneId);
	if (milestone && isClosedStatus(milestone.status)) {
		return {
			error: `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`,
		};
	}
	const slice = getSlice(params.milestoneId, params.sliceId);
	if (slice && isClosedStatus(slice.status)) {
		return {
			error: `slice ${params.sliceId} is already complete — use sf_slice_reopen first if you need to redo it`,
		};
	}
	const tasks = getSliceTasks(params.milestoneId, params.sliceId);
	if (tasks.length === 0) {
		return {
			error: `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`,
		};
	}
	const incompleteTasks = tasks.filter((t) => !isClosedStatus(t.status));
	if (incompleteTasks.length > 0) {
		const incompleteIds = incompleteTasks
			.map((t) => `${t.id} (status: ${t.status})`)
			.join(", ");
		return { error: `incomplete tasks: ${incompleteIds}` };
	}
	// ADR-0000 (SF v70) — purpose-trace gate ────────────────────────────────
	// complete-slice cannot flip a slice to complete unless every task
	// recorded a goal-anchored purpose_trace at complete_task time. This is
	// the mechanical check that "did all tasks actually serve the slice
	// goal" — without it the slice contract is decoupled from per-task
	// intent and the doctrine collapses to vibes.
	const tasksMissingTrace = tasks.filter((t) => {
		const trace = t?.purpose_trace ?? t?.purposeTrace;
		return typeof trace !== "string" || trace.trim().length === 0;
	});
	if (tasksMissingTrace.length > 0) {
		const missingIds = tasksMissingTrace.map((t) => t.id).join(", ");
		return {
			error:
				`cannot complete slice ${params.sliceId}: ${tasksMissingTrace.length} ` +
				`task(s) have no purpose_trace (ADR-0000): ${missingIds}. ` +
				`Reopen each task and re-run complete_task with the slice-goal sentence ` +
				`that change served, then retry complete_slice.`,
		};
	}
	// Render summary markdown
	const summaryMd = renderSliceSummaryMarkdown(params);
	// Resolve and write summary to disk
	let summaryPath;
	const sliceDir = resolveSlicePath(
		basePath,
		params.milestoneId,
		params.sliceId,
	);
	if (sliceDir) {
		summaryPath = join(sliceDir, `${params.sliceId}-SUMMARY.md`);
	} else {
		// Slice dir doesn't exist on disk yet — build path manually and ensure dirs
		const sfDir = join(basePath, ".sf");
		const manualSliceDir = join(
			sfDir,
			"milestones",
			params.milestoneId,
			"slices",
			params.sliceId,
		);
		mkdirSync(manualSliceDir, { recursive: true });
		summaryPath = join(manualSliceDir, `${params.sliceId}-SUMMARY.md`);
	}
	const uatMd = renderUatMarkdown(params);
	const uatPath = summaryPath.replace(/-SUMMARY\.md$/, "-UAT.md");
	// ── Filesystem commit before DB status mutation ────────────────────────
	// SUMMARY.md and UAT.md are the artifacts downstream agents read. If either
	// write fails, do not mutate the DB into a completed state.
	try {
		await writeMarkdownBeforeDb(summaryPath, summaryMd, "SUMMARY.md");
		await writeMarkdownBeforeDb(uatPath, uatMd, "UAT.md");
	} catch (writeErr) {
		logWarning(
			"tool",
			`complete_slice — artifact write failed before DB update for ${params.milestoneId}/${params.sliceId}`,
			{ error: errorMessage(writeErr) },
		);
		invalidateStateCache();
		return { error: errorMessage(writeErr) };
	}
	// ── DB commit only after SUMMARY.md and UAT.md exist on disk ───────────
	try {
		transaction(() => {
			insertMilestone({ id: params.milestoneId, title: params.milestoneId });
			insertSlice({
				id: params.sliceId,
				milestoneId: params.milestoneId,
				title: params.sliceId,
			});
			updateSliceStatus(
				params.milestoneId,
				params.sliceId,
				"complete",
				completedAt,
			);
			setSliceSummaryMd(params.milestoneId, params.sliceId, summaryMd, uatMd);
			// Record evidence: slice completion
			const sliceEvidenceContent = JSON.stringify({
				oneLiner: params.oneLiner ?? "",
				narrative: params.narrative ?? "",
				successCriteriaResults: params.successCriteriaResults ?? "",
				verificationProof: params.verificationProof ?? "",
				keyDecisions: params.keyDecisions ?? [],
				keyFiles: params.keyFiles ?? [],
			});
			insertSliceEvidence(
				params.milestoneId,
				params.sliceId,
				"completion_summary",
				sliceEvidenceContent,
				"complete-slice",
				"agent",
			);
		});
	} catch (dbErr) {
		const msg = errorMessage(dbErr);
		logError(
			"tool",
			`complete_slice — DB update failed after artifact writes succeeded; keeping ${summaryPath} and ${uatPath}`,
			{ error: msg },
		);
		invalidateStateCache();
		return {
			error: `database update failed after SUMMARY.md/UAT.md write succeeded at ${summaryPath}: ${msg}. Files were kept; retry complete_slice after fixing the DB.`,
		};
	}
	// Toggle roadmap checkbox via renderer module after DB status is updated.
	try {
		const roadmapToggled = await renderRoadmapCheckboxes(
			basePath,
			params.milestoneId,
		);
		if (!roadmapToggled) {
			logWarning(
				"tool",
				`complete_slice — could not find roadmap for ${params.milestoneId}, skipping checkbox toggle`,
			);
		}
	} catch (renderErr) {
		logWarning(
			"tool",
			`complete_slice — roadmap checkbox render failed after DB update for ${params.milestoneId}/${params.sliceId}`,
			{ error: errorMessage(renderErr) },
		);
	}
	// ── Close gates owned by complete-slice (Q8) ───────────────────────────
	// Each owned gate maps to a specific summary section via the registry.
	// If the caller populated the corresponding field, record `pass`; if the
	// field is empty, record `omitted`. Without this loop, Q8 would stay
	// pending forever and block future state derivation (see gate-registry).
	try {
		const pendingGates = getPendingGatesForTurn(
			params.milestoneId,
			params.sliceId,
			"complete-slice",
		);
		if (pendingGates.length > 0) {
			const ownedDefs = new Map(
				getGatesForTurn("complete-slice").map((g) => [g.id, g]),
			);
			for (const row of pendingGates) {
				const def = ownedDefs.get(row.gate_id);
				if (!def) continue;
				// Map gate id → param field it maps to. Keep the map local so
				// adding a new complete-slice gate is a single place change.
				const field = sliceGateFieldForId(def.id, params);
				const hasContent = typeof field === "string" && field.trim().length > 0;
				saveGateResult({
					milestoneId: params.milestoneId,
					sliceId: params.sliceId,
					gateId: def.id,
					verdict: hasContent ? "pass" : "omitted",
					rationale: hasContent
						? `${def.promptSection} section populated in slice summary`
						: `${def.promptSection} section left empty — recorded as omitted`,
					findings: hasContent ? field.trim() : "",
				});
			}
		}
	} catch (gateErr) {
		logWarning(
			"tool",
			`complete-slice gate close warning for ${params.milestoneId}/${params.sliceId}: ${gateErr.message}`,
		);
	}
	// Invalidate all caches
	invalidateStateCache();
	clearPathCache();
	clearParseCache();
	// ── Post-mutation hook: projections, manifest, event log ───────────────
	// Separate try/catch per step so a projection failure doesn't prevent
	// the event log entry (critical for worktree reconciliation).
	try {
		await renderAllProjections(basePath, params.milestoneId);
	} catch (projErr) {
		logWarning(
			"tool",
			`complete-slice projection warning for ${params.milestoneId}/${params.sliceId}: ${projErr.message}`,
		);
	}
	try {
		writeManifest(basePath);
	} catch (mfErr) {
		logWarning("tool", `complete-slice manifest warning: ${mfErr.message}`);
	}
	try {
		appendEvent(basePath, {
			cmd: "complete-slice",
			params: { milestoneId: params.milestoneId, sliceId: params.sliceId },
			ts: new Date().toISOString(),
			actor: "agent",
			actor_name: params.actorName,
			trigger_reason: params.triggerReason,
		});
	} catch (eventErr) {
		logError(
			"tool",
			`complete-slice event log FAILED — completion invisible to reconciliation`,
			{ error: eventErr.message },
		);
	}
	// Fire-and-forget graph rebuild — must NOT await, must NOT crash slice completion.
	// Dynamic import of the package name (not a relative path) so it resolves
	// correctly via package.json#exports in both development and production.
	// eslint-disable-next-line @typescript-eslint/no-floating-promises
	(async () => {
		try {
			const graphMod = await import("@singularity-forge/agent-core");
			if (
				typeof graphMod.buildGraph !== "function" ||
				typeof graphMod.writeGraph !== "function" ||
				typeof graphMod.resolveSFRoot !== "function"
			) {
				throw new Error(
					"graph helpers unavailable from @singularity-forge/agent-core",
				);
			}
			const g = await graphMod.buildGraph(basePath);
			await graphMod.writeGraph(graphMod.resolveSFRoot(basePath), g);
		} catch (graphErr) {
			// Graph rebuild is best-effort — log at warning level but never propagate
			logWarning(
				"tool",
				`complete-slice graph rebuild failed (non-fatal): ${graphErr.message ?? String(graphErr)}`,
			);
		}
	})();
	return {
		sliceId: params.sliceId,
		milestoneId: params.milestoneId,
		summaryPath,
		uatPath,
	};
}
