/**
 * Scaffold-keeper background dispatcher (ADR-021 Phase D).
 *
 * Runs after milestone completion (and any other place SF chooses to call it).
 * Walks the drift report, and for every `editing-drift` item produces a
 * `<file>.proposed` artifact and emits a single structured `approval_request`
 * notification so the user can review the proposal.
 *
 * Phase D ships the architecture and a deterministic stub for the proposed
 * body: the current scaffold template content with a `<!-- sf-proposed: ... -->`
 * preamble. A follow-up phase will replace the body generator with a real
 * subagent dispatch that runs the `records-keeper` skill — at that point the
 * `<file>.proposed` body will be code-derived (records-keeper's "prefer source
 * and tests for implemented behavior" contract) instead of template-only.
 *
 * Contract:
 *   - Non-blocking. Caller does not await the body. Failures are non-fatal:
 *     they log via `logWarning("scaffold", ...)` and never propagate.
 *   - Never overwrites `<file>` itself — only writes `<file>.proposed`.
 *   - Emits exactly one `approval_request` notification per call when there is
 *     at least one editing-drift item; the stable `dedupe_key` prevents the
 *     same drift from spamming the user across repeated runs.
 *   - Silent path: when there are zero editing-drift items, no notification,
 *     no .proposed writes, no logs.
 */
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { SCAFFOLD_FILES } from "./agentic-docs-scaffold.js";
import { getErrorMessage } from "./error-utils.js";
import { detectScaffoldDrift } from "./scaffold-drift.js";
import { logWarning } from "./workflow-logger.js";

/**
 * Build the .proposed body shipped by the Phase-D stub: the current scaffold
 * template body, prefixed with a structured `<!-- sf-proposed: ... -->` block
 * describing source vs target version. This matches ADR-021 §5 ("editing-drift").
 *
 * The records-keeper skill is referenced in the preamble so a human reviewing
 * the artifact knows which skill the agent will run when wired up.
 */
function buildProposedBody(item, templateBody) {
	const sourceVersion = item.currentVersion ?? "unknown";
	const targetVersion = item.shipVersion;
	const preamble =
		`<!-- sf-proposed: source=${sourceVersion} target=${targetVersion} ` +
		`template=${item.template} skill=records-keeper ` +
		`note="Stub body — replace with code-derived content via records-keeper subagent." ` +
		`-->\n`;
	return preamble + templateBody;
}
/**
 * Scaffold-keeper entry point. Fire-and-forget — caller should not await
 * meaningful work, only the inevitable I/O completion.
 *
 * Steps:
 *   1. detect drift; bail silently if no editing-drift items.
 *   2. for each editing-drift item, write `<file>.proposed` with the stub
 *      body. Skip files with no matching `SCAFFOLD_FILES` entry (defensive).
 *   3. emit a single `approval_request` notification summarizing the count.
 *
 * Returns the number of `.proposed` files written. Tests use this to assert
 * the smoke-check behaviour; production callers ignore the return value.
 */
export async function dispatchScaffoldKeeperIfNeeded(basePath, ctx) {
	let report;
	try {
		report = detectScaffoldDrift(basePath);
	} catch (err) {
		logWarning("scaffold", "scaffold-keeper drift detection failed", {
			error: err.message,
		});
		return 0;
	}
	if (report.countsByBucket["editing-drift"] === 0) {
		return 0;
	}
	const editingDrift = report.items.filter((i) => i.bucket === "editing-drift");
	let written = 0;
	for (const item of editingDrift) {
		try {
			const template = SCAFFOLD_FILES.find((f) => f.path === item.template);
			if (!template) {
				logWarning("scaffold", "scaffold-keeper: no template for drift item", {
					path: item.path,
				});
				continue;
			}
			const proposedPath = join(basePath, `${item.path}.proposed`);
			const proposedBody = buildProposedBody(item, template.content);
			mkdirSync(dirname(proposedPath), { recursive: true });
			writeFileSync(proposedPath, proposedBody, "utf-8");
			written += 1;
		} catch (err) {
			logWarning("scaffold", "scaffold-keeper: failed to write .proposed", {
				path: item.path,
				error: err.message,
			});
		}
	}
	// Stub notice — when subagent dispatch is wired, this log line goes away
	// and the body becomes code-derived rather than template-only.
	logWarning(
		"scaffold",
		"scaffold-keeper agent dispatch not yet wired — wrote .proposed with template only",
		{ count: String(written) },
	);
	if (written > 0) {
		try {
			ctx.ui.notify(
				`Scaffold drift: ${written} doc(s) need review. See .proposed files.`,
				"warning",
				{
					kind: "approval_request",
					blocking: false,
					dedupe_key: "scaffold-drift",
					source: "scaffold-keeper",
				},
			);
		} catch (err) {
			logWarning("scaffold", "scaffold-keeper: notify failed", {
				error: err.message,
			});
		}
	}
	return written;
}
/**
 * Sync wrapper: schedules `dispatchScaffoldKeeperIfNeeded` without blocking the
 * caller. Errors swallowed and logged so a scaffold-keeper failure cannot break
 * the auto loop. Used from auto-post-unit and auto.ts:stopAuto where the
 * caller cannot reasonably `await` async work without re-architecting cleanup.
 */
export function dispatchScaffoldKeeperFireAndForget(basePath, ctx) {
	// Use queueMicrotask so the dispatch starts after the current sync stack
	// unwinds — the caller's flow remains synchronous and unblocked.
	queueMicrotask(() => {
		dispatchScaffoldKeeperIfNeeded(basePath, ctx).catch((err) => {
			logWarning("scaffold", "scaffold-keeper dispatch threw", {
				error: getErrorMessage(err),
			});
		});
	});
}
// Test-only helper: not exported via index, but exposed for the smoke test.
export const __test = {
	buildProposedBody,
};
