/**
 * Doc Scaffold Checker — validates the agentic docs scaffold is filled in.
 *
 * Purpose: Mechanical enforcement of harness-engineering principles. After
 * bootstrap or milestone close, check that scaffold files contain real content
 * beyond the template stubs. Reports findings so the agent knows what needs
 * attention — never blocks, only surfaces.
 *
 * ADR-022: Only checks files in the active profile. Files with `state=disabled`
 * are skipped; they are intentionally out of scope for this repo.
 *
 * Consumer: bootstrapProject (after scaffold init), milestone close workflows.
 */
import { existsSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { SCAFFOLD_FILES } from "./scaffold-constants.js";
import { resolveActiveProfileSet } from "./scaffold-profiles.js";
import { extractMarker, readScaffoldManifest } from "./scaffold-versioning.js";

/** Files created by ensureAgenticDocsScaffold that should contain real content.
 * Kept for compatibility — checkDocsScaffold now derives its list from the
 * active profile at runtime (ADR-022). */
const STUB_ALLOWED_PATHS = new Set([
	"docs/product-specs/index.md",
	"docs/design-docs/index.md",
	"docs/exec-plans/active/index.md",
	"docs/exec-plans/completed/index.md",
	"docs/records/index.md",
	// AGENTS.md scaffolds that are intentionally short index/header files
	// pointing at the actual content in sibling directories.
	"docs/AGENTS.md",
	"docs/records/AGENTS.md",
	".sf/harness/AGENTS.md",
	".sf/harness/specs/AGENTS.md",
	".sf/harness/evals/AGENTS.md",
	".sf/harness/graders/AGENTS.md",
	// Top-level .sf/ project meta-files: terse-by-design bullet lists. The
	// `# Heading` line is stripped by countContentLines so a 9-bullet file
	// counts as 9 content lines — sub-threshold but substantive.
	".sf/PRINCIPLES.md",
	".sf/STYLE.md",
	".sf/NON-GOALS.md",
]);
// Minimum lines considered "real content" vs stub. Template stubs are ~3-8 lines.
const STUB_LINE_COUNT = 10;
function countContentLines(content) {
	// Count non-empty, non-comment lines
	return content.split("\n").filter((line) => {
		const trimmed = line.trim();
		return (
			trimmed.length > 0 &&
			!trimmed.startsWith("//") &&
			!trimmed.startsWith("#")
		);
	}).length;
}
function checkFile(repoRoot, relPath) {
	const fullPath = join(repoRoot, relPath);
	if (!existsSync(fullPath)) {
		return {
			file: relPath,
			status: "missing",
			lines: 0,
			note: "File does not exist — scaffold not run or was interrupted",
		};
	}
	let content;
	try {
		const stat = statSync(fullPath);
		if (stat.isDirectory()) {
			return {
				file: relPath,
				status: "stub",
				lines: 0,
				note: "Is a directory, expected a file",
			};
		}
		content = readFileSync(fullPath, "utf-8");
	} catch {
		return {
			file: relPath,
			status: "stub",
			lines: 0,
			note: "Could not read file",
		};
	}
	const lines = content.split("\n").filter((l) => l.trim().length > 0).length;
	const contentLines = countContentLines(content);
	if (lines === 0) {
		return { file: relPath, status: "empty", lines: 0, note: "File is empty" };
	}
	if (contentLines < STUB_LINE_COUNT) {
		const note = STUB_ALLOWED_PATHS.has(relPath)
			? `Stub file (${lines} lines) — acceptable for index/placeholder`
			: `Stub file (${lines} lines) — needs real content beyond template`;
		return {
			file: relPath,
			status: STUB_ALLOWED_PATHS.has(relPath) ? "ok" : "stub",
			lines,
			note,
		};
	}
	return {
		file: relPath,
		status: "ok",
		lines,
		note: `Contains ${contentLines} content lines`,
	};
}
/**
 * Check scaffold files in a repo. Returns a structured report.
 * Never throws — all errors are caught and reported as stub/missing.
 *
 * ADR-022: filters to the active profile and skips `state=disabled` files.
 * Files not in the active profile are not checked (they don't apply to this
 * repo shape). Files with `state=disabled` are skipped as intentionally
 * out-of-scope.
 */
export function checkDocsScaffold(repoRoot) {
	const manifest = readScaffoldManifest(repoRoot);
	const { profileSet } = resolveActiveProfileSet(repoRoot, manifest, null);
	const checks = [];
	for (const file of SCAFFOLD_FILES) {
		// Skip files not in the active profile.
		if (!profileSet.has(file.path)) continue;
		// Skip files with state=disabled — intentionally out of scope.
		try {
			const { marker } = extractMarker(join(repoRoot, file.path));
			if (marker?.state === "disabled") continue;
		} catch {
			// File unreadable — fall through to normal check.
		}
		checks.push(checkFile(repoRoot, file.path));
	}
	const summary = {
		total: checks.length,
		ok: checks.filter((c) => c.status === "ok").length,
		empty: checks.filter((c) => c.status === "empty").length,
		stub: checks.filter((c) => c.status === "stub").length,
		missing: checks.filter((c) => c.status === "missing").length,
	};
	return {
		checkedAt: new Date().toISOString(),
		repoRoot,
		checks,
		summary,
	};
}
/**
 * Format a report as human-readable text for logging to stderr.
 */
export function formatDocCheckReport(report) {
	const lines = [];
	lines.push(`[doc-checker] Scaffold check — ${report.checkedAt}`);
	lines.push(
		`  ${report.summary.ok}/${report.summary.total} files OK` +
			(report.summary.stub > 0
				? ` · ${report.summary.stub} need content`
				: "") +
			(report.summary.missing > 0
				? ` · ${report.summary.missing} missing`
				: "") +
			(report.summary.empty > 0 ? ` · ${report.summary.empty} empty` : ""),
	);
	const issues = report.checks.filter((c) => c.status !== "ok");
	if (issues.length > 0) {
		lines.push("  Files needing attention:");
		for (const issue of issues) {
			lines.push(`    [${issue.status}] ${issue.file} — ${issue.note}`);
		}
	} else {
		lines.push("  All scaffold files contain real content.");
	}
	return lines.join("\n");
}
