// SF — Product Completeness Audit tool
//
// Slim implementation of the milestone-end product-audit workflow phase.
// The tool name is `audit_product`. It validates a structured audit
// payload (verdict + gaps) and writes the result to:
//   .sf/active/{milestoneId}/PRODUCT-AUDIT.json
//   .sf/active/{milestoneId}/PRODUCT-AUDIT.md
//
// This is a soft gate — it does not hard-block milestone completion.
// Follow-up slice scheduling for actionable gaps lives outside this tool.
import { join } from "node:path";
import { atomicWriteAsync } from "../atomic-write.js";
import { checkSafeIds } from "../safety/safe-id.js";
import { isNonEmptyString } from "../validation.js";
export const PRODUCT_GAP_SEVERITIES = ["critical", "high", "medium", "low"];
export const PRODUCT_AUDIT_VERDICTS = [
	"no-gaps",
	"gaps-found",
	"contract-underspecified",
];
function validateStringArray(value, fieldName) {
	if (!Array.isArray(value)) {
		throw new Error(`${fieldName} must be an array`);
	}
	for (let i = 0; i < value.length; i++) {
		if (typeof value[i] !== "string") {
			throw new Error(`${fieldName}[${i}] must be a string`);
		}
	}
	return value;
}
function validateSuggestedSlice(value, fieldName) {
	if (!value || typeof value !== "object") {
		throw new Error(`${fieldName} must be an object`);
	}
	const slice = value;
	if (!isNonEmptyString(slice.title)) {
		throw new Error(`${fieldName}.title is required`);
	}
	if (!isNonEmptyString(slice.demo)) {
		throw new Error(`${fieldName}.demo is required`);
	}
	if (!isNonEmptyString(slice.risk)) {
		throw new Error(`${fieldName}.risk is required`);
	}
	const depends = validateStringArray(
		slice.depends ?? [],
		`${fieldName}.depends`,
	);
	return {
		title: slice.title,
		demo: slice.demo,
		risk: slice.risk,
		depends,
	};
}
export function validateProductAuditParams(params) {
	if (!params || typeof params !== "object") {
		throw new Error("params must be an object");
	}
	if (!isNonEmptyString(params.milestoneId)) {
		throw new Error("milestoneId is required");
	}
	if (!PRODUCT_AUDIT_VERDICTS.includes(params.verdict)) {
		throw new Error(
			`verdict must be one of: ${PRODUCT_AUDIT_VERDICTS.join(", ")}`,
		);
	}
	if (!isNonEmptyString(params.summary)) {
		throw new Error("summary is required");
	}
	if (!Array.isArray(params.gaps)) {
		throw new Error("gaps must be an array");
	}
	const gaps = [];
	for (let i = 0; i < params.gaps.length; i++) {
		const gap = params.gaps[i];
		if (!gap || typeof gap !== "object") {
			throw new Error(`gaps[${i}] must be an object`);
		}
		if (!isNonEmptyString(gap.capability)) {
			throw new Error(`gaps[${i}].capability is required`);
		}
		const expectedEvidence = validateStringArray(
			gap.expectedEvidence,
			`gaps[${i}].expectedEvidence`,
		);
		const foundEvidence = validateStringArray(
			gap.foundEvidence,
			`gaps[${i}].foundEvidence`,
		);
		const missingEvidence = validateStringArray(
			gap.missingEvidence,
			`gaps[${i}].missingEvidence`,
		);
		if (!PRODUCT_GAP_SEVERITIES.includes(gap.severity)) {
			throw new Error(
				`gaps[${i}].severity must be one of: ${PRODUCT_GAP_SEVERITIES.join(", ")}`,
			);
		}
		const suggestedSlice = validateSuggestedSlice(
			gap.suggestedSlice,
			`gaps[${i}].suggestedSlice`,
		);
		if (
			typeof gap.confidence !== "number" ||
			!Number.isFinite(gap.confidence) ||
			gap.confidence < 0 ||
			gap.confidence > 1
		) {
			throw new Error(`gaps[${i}].confidence must be a number from 0 to 1`);
		}
		const sourceDocs = validateStringArray(
			gap.sourceDocs,
			`gaps[${i}].sourceDocs`,
		);
		gaps.push({
			capability: gap.capability,
			expectedEvidence,
			foundEvidence,
			missingEvidence,
			severity: gap.severity,
			suggestedSlice,
			confidence: gap.confidence,
			sourceDocs,
		});
	}
	return {
		milestoneId: params.milestoneId,
		verdict: params.verdict,
		summary: params.summary,
		gaps,
	};
}
function renderEvidenceList(values) {
	return values.length > 0
		? values.map((value) => `- ${value}`).join("\n")
		: "- None";
}
function renderAuditMarkdown(params, actionableGaps) {
	const gapTable =
		params.gaps.length > 0
			? params.gaps
					.map(
						(gap) =>
							`| ${gap.capability} | ${gap.severity} | ${gap.confidence.toFixed(2)} | ${gap.sourceDocs.join(", ") || "None"} |`,
					)
					.join("\n")
			: "| None | n/a | n/a | n/a |";
	const gapDetails =
		params.gaps.length > 0
			? params.gaps
					.map((gap, index) =>
						[
							`## Gap ${index + 1}: ${gap.capability}`,
							"",
							`- Severity: ${gap.severity}`,
							`- Confidence: ${gap.confidence.toFixed(2)}`,
							`- Suggested slice: ${gap.suggestedSlice.title}`,
							`- Source docs: ${gap.sourceDocs.join(", ") || "None"}`,
							"",
							"### Expected Evidence",
							renderEvidenceList(gap.expectedEvidence),
							"",
							"### Found Evidence",
							renderEvidenceList(gap.foundEvidence),
							"",
							"### Missing Evidence",
							renderEvidenceList(gap.missingEvidence),
						].join("\n"),
					)
					.join("\n\n")
			: "## Gaps\n\nNo material product gaps were found with positive evidence.";
	return [
		"---",
		`verdict: ${params.verdict}`,
		`gap_count: ${params.gaps.length}`,
		`actionable_gap_count: ${actionableGaps.length}`,
		`created_at: ${new Date().toISOString()}`,
		"---",
		"",
		`# Product Audit: ${params.milestoneId}`,
		"",
		"## Summary",
		"",
		params.summary.trim(),
		"",
		"## Gap Index",
		"",
		"| Capability | Severity | Confidence | Source docs |",
		"|---|---:|---:|---|",
		gapTable,
		"",
		gapDetails,
		"",
	].join("\n");
}
export async function handleProductAudit(rawParams, basePath) {
	let params;
	try {
		params = validateProductAuditParams(rawParams);
	} catch (err) {
		return { error: `validation failed: ${err.message}` };
	}
	// Reject path-traversal in any ID before it reaches 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: "unsafe_id",
			field: idCheck.fieldName,
			reason: idCheck.reason,
		};
	}
	const actionableGaps = params.gaps.filter(
		(gap) => gap.severity === "critical" || gap.severity === "high",
	);
	const auditDir = join(basePath, ".sf", "active", params.milestoneId);
	const jsonPath = join(auditDir, "PRODUCT-AUDIT.json");
	const markdownPath = join(auditDir, "PRODUCT-AUDIT.md");
	const jsonPayload = {
		milestoneId: params.milestoneId,
		verdict: params.verdict,
		summary: params.summary,
		gapCount: params.gaps.length,
		actionableGapCount: actionableGaps.length,
		createdAt: new Date().toISOString(),
		gaps: params.gaps,
	};
	try {
		await atomicWriteAsync(
			jsonPath,
			`${JSON.stringify(jsonPayload, null, 2)}\n`,
		);
		await atomicWriteAsync(
			markdownPath,
			renderAuditMarkdown(params, actionableGaps),
		);
	} catch (err) {
		return { error: `audit save failed: ${err.message}` };
	}
	return {
		milestoneId: params.milestoneId,
		verdict: params.verdict,
		gapCount: params.gaps.length,
		actionableGapCount: actionableGaps.length,
		jsonPath,
		markdownPath,
	};
}
