/**
 * validate-milestone handler — the core operation behind validate_milestone.
 *
 * Persists milestone validation results to the assessments table and
 * quality_gates table, renders VALIDATION.md to disk, and invalidates caches.
 *
 * #2945 Bug 4: Previously only wrote to assessments — quality_gates records
 * were never persisted, causing M002+ milestones to have zero gate records
 * despite passing validation.
 */
import { join } from "node:path";
import { clearParseCache, saveFile } from "../files.js";
import { insertMilestoneValidationGates } from "../milestone-validation-gates.js";
import { clearPathCache, resolveMilestonePath } from "../paths.js";
import { loadEffectiveSFPreferences } from "../preferences.js";
import { checkSafeIds } from "../safety/safe-id.js";
import {
	deleteAssessmentByScope,
	getMilestoneSlices,
	insertAssessment,
	transaction,
} from "../sf-db.js";
import { invalidateStateCache } from "../state.js";
import { resolveUokFlags } from "../uok/flags.js";
import { getGateRegistry } from "../uok/gate-registry.js";
import {
	isValidMilestoneVerdict,
	VALIDATION_VERDICTS,
} from "../verdict-parser.js";
import { logWarning } from "../workflow-logger.js";
import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";

/**
 * Module-scope gate definition for ADR-0075 UokGateRegistry.
 *
 * ctx shape (supplied via ctxFactory at call site):
 *   verdict        {string}  — raw milestone verdict ("pass" | "fail" | …)
 *   verdictRationale {string|undefined}
 *   remediationPlan  {string|undefined}
 *
 * The execute() function is pure relative to ctx — it no longer closes over
 * call-site locals, making the gate safe to register once at module load time.
 */
const milestoneValidationGatesGate = {
	id: "milestone-validation-gates",
	type: "verification",
	execute: async (ctx) => {
		const nonPassVerdict = ctx.verdict !== "pass";
		return {
			outcome: nonPassVerdict ? "manual-attention" : "pass",
			failureClass: nonPassVerdict ? "manual-attention" : "none",
			rationale: `milestone validation verdict: ${ctx.verdict}`,
			findings: nonPassVerdict
				? [ctx.verdictRationale, ctx.remediationPlan ?? ""]
						.filter(Boolean)
						.join("\n")
				: "",
		};
	},
};

function renderValidationMarkdown(params) {
	let md = `<!-- generated from .sf/sf.db — do not edit directly; use validate_milestone tool -->
---
verdict: ${params.verdict}
remediation_round: ${params.remediationRound}
---

# Milestone Validation: ${params.milestoneId}

## Success Criteria Checklist
${params.successCriteriaChecklist}

## Slice Delivery Audit
${params.sliceDeliveryAudit}

## Cross-Slice Integration
${params.crossSliceIntegration}

## Requirement Coverage
${params.requirementCoverage}

${
	params.verificationClasses
		? `## Verification Class Compliance
${params.verificationClasses}

`
		: ""
}
## Verdict Rationale
${params.verdictRationale}
`;
	if (params.verdict !== "pass" && params.remediationPlan) {
		md += `\n## Remediation Plan\n${params.remediationPlan}\n`;
	}
	return md;
}
export async function handleValidateMilestone(params, basePath, opts) {
	if (
		!params.milestoneId ||
		typeof params.milestoneId !== "string" ||
		params.milestoneId.trim() === ""
	) {
		return { error: "milestoneId is required and must be a non-empty string" };
	}
	if (!isValidMilestoneVerdict(params.verdict)) {
		return {
			error: `verdict must be one of: ${VALIDATION_VERDICTS.join(", ")}`,
		};
	}
	// Reject path-traversal in any ID before they reach string interpolation
	// below — this is the chokepoint. See safety/safe-id.ts for rationale and
	// the validation-safe-id-path-segments self-feedback entry.
	const idCheck = checkSafeIds({ milestoneId: params.milestoneId });
	if (idCheck) {
		return {
			error: idCheck.message,
			details: {
				error: "unsafe_id",
				field: idCheck.fieldName,
				reason: idCheck.reason,
			},
		};
	}
	// ── Resolve paths and render markdown ────────────────────────────────
	const validationMd = renderValidationMarkdown(params);
	// #4761: route through the canonical-root resolver so that when a live
	// worktree exists for this milestone, validation reads/writes the
	// worktree's artifacts instead of stale project-root state.
	const canonicalBase = resolveCanonicalMilestoneRoot(
		basePath,
		params.milestoneId,
	);
	let validationPath;
	const milestoneDir = resolveMilestonePath(canonicalBase, params.milestoneId);
	if (milestoneDir) {
		validationPath = join(milestoneDir, `${params.milestoneId}-VALIDATION.md`);
	} else {
		const sfDir = join(canonicalBase, ".sf");
		const manualDir = join(sfDir, "milestones", params.milestoneId);
		validationPath = join(manualDir, `${params.milestoneId}-VALIDATION.md`);
	}
	// ── DB write first — matches complete-task/complete-slice pattern ───
	// Write DB before disk so a crash between the two leaves a recoverable
	// state: the DB row exists but the file is missing, which projection
	// rendering can regenerate. The inverse (file exists, no DB row) is
	// harder to detect and recover from (#2725).
	const validatedAt = new Date().toISOString();
	const slices = getMilestoneSlices(params.milestoneId);
	const gateSliceId = slices.length > 0 ? slices[0].id : "_milestone";
	transaction(() => {
		insertAssessment({
			path: validationPath,
			milestoneId: params.milestoneId,
			sliceId: null,
			taskId: null,
			status: params.verdict,
			scope: "milestone-validation",
			fullContent: validationMd,
		});
		// #2945 Bug 4: persist quality_gates records alongside the assessment.
		// Previously only the assessment was written, leaving M002+ milestones
		// with zero quality_gate records despite passing validation.
		insertMilestoneValidationGates(
			params.milestoneId,
			gateSliceId,
			params.verdict,
			validatedAt,
		);
	});
	// ── Filesystem render (outside transaction) ────────────────────────────
	// If disk render fails, roll back the DB row so state stays consistent.
	try {
		await saveFile(validationPath, validationMd);
	} catch (renderErr) {
		logWarning(
			"tool",
			`validate_milestone — disk render failed, rolling back DB row: ${renderErr.message}`,
		);
		deleteAssessmentByScope(params.milestoneId, "milestone-validation");
		return { error: `disk render failed: ${renderErr.message}` };
	}
	invalidateStateCache();
	clearPathCache();
	clearParseCache();
	const prefs = loadEffectiveSFPreferences()?.preferences;
	const gatesEnabled = opts?.uokGatesEnabled ?? resolveUokFlags(prefs).gates;
	if (gatesEnabled) {
		try {
			const registry = getGateRegistry();
			if (!registry.has(milestoneValidationGatesGate.id)) {
				registry.register(milestoneValidationGatesGate);
			}
			await registry.run(milestoneValidationGatesGate.id, {
				// ctxFactory: lifted closure state supplied as per-call override
				verdict: params.verdict,
				verdictRationale: params.verdictRationale,
				remediationPlan: params.remediationPlan,
				// runner ctx fields
				basePath,
				traceId: opts?.traceId ?? `validate-milestone:${params.milestoneId}`,
				turnId: opts?.turnId ?? `${params.milestoneId}:validate`,
				milestoneId: params.milestoneId,
				sliceId: gateSliceId,
				unitType: "validate-milestone",
				unitId: params.milestoneId,
			});
		} catch (err) {
			logWarning(
				"tool",
				`validate_milestone — failed to persist UOK gate result: ${err.message}`,
			);
		}
	}
	return {
		milestoneId: params.milestoneId,
		verdict: params.verdict,
		validationPath,
	};
}
