/**
 * commands-scaffold-sync.ts — `/scaffold sync` (ADR-021 Phase E).
 *
 * Manual escape hatch over the Phase C automatic scaffold sync. Lets the user:
 *  - Inspect drift without modifying anything (`--dry-run`).
 *  - Force the same operation that would run on next SF startup (default).
 *  - Run scaffold-keeper synchronously for editing-drift items
 *    (`--include-editing`) when Phase D has shipped.
 *  - Restrict the operation to a path glob (`--only=<glob>`).
 *
 * The command is intentionally thin: it dispatches to
 * `ensureAgenticDocsScaffold` and renders `detectScaffoldDrift`. It does not
 * reimplement either.
 */
import { ensureAgenticDocsScaffold } from "./agentic-docs-scaffold.js";
import { projectRoot } from "./commands/context.js";
import { detectScaffoldDrift } from "./scaffold-drift.js";
/** Parse the args string for `/scaffold sync`. Tolerates extra whitespace. */
export function parseScaffoldSyncArgs(args) {
	const trimmed = (args || "").trim();
	const tokens = trimmed.length > 0 ? trimmed.split(/\s+/) : [];
	const opts = {
		dryRun: false,
		includeEditing: false,
	};
	for (const tok of tokens) {
		if (tok === "--dry-run") {
			opts.dryRun = true;
		} else if (tok === "--include-editing") {
			opts.includeEditing = true;
		} else if (tok.startsWith("--only=")) {
			const value = tok.slice("--only=".length).trim();
			if (value.length > 0) opts.only = value;
		}
	}
	return opts;
}
/**
 * Match a scaffold path against an `--only=<glob>` value.
 *
 * Supports the simple cases the brief calls out: `*` is treated as a wildcard,
 * and as a fallback we accept plain prefix or suffix matches. We deliberately
 * do not pull in a glob library — Phase E is the escape hatch, not a
 * production globber.
 */
export function matchesOnly(path, glob) {
	if (!glob) return true;
	if (path === glob) return true;
	if (glob.includes("*")) {
		// Build a forgiving regex: escape regex metachars, then turn `*` into `.*`.
		const pattern = glob
			.split("*")
			.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
			.join(".*");
		try {
			return new RegExp(`^${pattern}$`).test(path);
		} catch {
			return false;
		}
	}
	// Plain string: accept prefix or suffix match. Suffix is useful for
	// `--only=RELIABILITY.md`; prefix for `--only=harness/`.
	return path.startsWith(glob) || path.endsWith(glob);
}
/** Filter a drift report's items by an --only glob. Counts are recomputed. */
export function applyOnlyFilter(report, only) {
	if (!only) return report;
	const items = report.items.filter((i) => matchesOnly(i.path, only));
	const counts = {
		missing: 0,
		upgradable: 0,
		"editing-drift": 0,
		untracked: 0,
		customized: 0,
	};
	for (const item of items) {
		counts[item.bucket] += 1;
	}
	return {
		items,
		countsByBucket: counts,
		manifestPresent: report.manifestPresent,
	};
}
function formatReportTable(report) {
	const c = report.countsByBucket;
	// Per ADR-021 §10 the user-facing label for the `upgradable` drift bucket is
	// "Pending" — those are pending-state files whose stamped version trails the
	// current ship version and are slated for silent re-render on next sync.
	// The `customized` bucket includes both truly-customized files and synced files
	// with no pending action; use hashDrifted field to distinguish if needed.
	const lines = [
		"Scaffold drift report:",
		`  Missing      : ${c.missing}`,
		`  Pending      : ${c.upgradable}`,
		`  Editing-drift: ${c["editing-drift"]}`,
		`  Untracked    : ${c.untracked}`,
		`  No-action    : ${c.customized}`,
	];
	const review = report.items.filter(
		(i) => i.bucket === "missing" || i.bucket === "editing-drift",
	);
	if (review.length > 0) {
		lines.push("");
		lines.push("Items needing review:");
		for (const item of review) {
			lines.push(`  ${item.path}  (${item.bucket})`);
		}
	}
	return lines.join("\n");
}
/**
 * Format a brief deltas summary comparing pre- and post-sync drift reports.
 * Helps the user see at a glance what the sync actually did.
 */
function formatSyncDelta(before, after) {
	const wroteMissing =
		before.countsByBucket.missing - after.countsByBucket.missing;
	const upgraded =
		before.countsByBucket.upgradable - after.countsByBucket.upgradable;
	const promoted =
		before.countsByBucket.untracked - after.countsByBucket.untracked;
	if (wroteMissing <= 0 && upgraded <= 0 && promoted <= 0) return null;
	const parts = [];
	if (wroteMissing > 0) parts.push(`wrote ${wroteMissing} missing`);
	if (upgraded > 0) parts.push(`refreshed ${upgraded} pending`);
	if (promoted > 0) parts.push(`promoted ${promoted} legacy-matched`);
	return `Sync complete — ${parts.join(", ")}.`;
}
/**
 * Lazy import for Phase D's scaffold-keeper dispatcher. Returns `null` if
 * Phase D has not shipped yet, in which case `--include-editing` reports the
 * feature as unavailable rather than crashing.
 */
async function tryLoadScaffoldKeeper() {
	try {
		const mod = await import("./scaffold-keeper.js").catch(() => null);
		if (mod && typeof mod.dispatchScaffoldKeeperIfNeeded === "function") {
			return mod.dispatchScaffoldKeeperIfNeeded;
		}
	} catch {
		// fall through
	}
	return null;
}
/**
 * Top-level handler for `/scaffold sync [args]`.
 *
 * Always notifies via `ctx.ui.notify` — never throws on the sync paths
 * themselves; underlying calls (`ensureAgenticDocsScaffold`,
 * `detectScaffoldDrift`) are non-throwing per their contracts.
 */
export async function handleScaffoldSync(args, ctx) {
	const opts = parseScaffoldSyncArgs(args);
	const basePath = projectRoot();
	// Dry-run: report only, no filesystem mutation.
	if (opts.dryRun) {
		const report = applyOnlyFilter(detectScaffoldDrift(basePath), opts.only);
		ctx.ui.notify(formatReportTable(report), "info");
		return;
	}
	// Default: run the same automatic-mode entry point, then report.
	const before = applyOnlyFilter(detectScaffoldDrift(basePath), opts.only);
	try {
		ensureAgenticDocsScaffold(basePath);
	} catch (err) {
		ctx.ui.notify(`Scaffold sync failed: ${err.message}`, "warning");
		return;
	}
	const after = applyOnlyFilter(detectScaffoldDrift(basePath), opts.only);
	const delta = formatSyncDelta(before, after);
	const reportText = formatReportTable(after);
	const message = delta ? `${delta}\n\n${reportText}` : reportText;
	ctx.ui.notify(message, "info");
	if (!opts.includeEditing) return;
	// --include-editing: synchronously dispatch Phase D's keeper for editing-drift.
	const editingItems = after.items.filter((i) => i.bucket === "editing-drift");
	if (editingItems.length === 0) {
		ctx.ui.notify("No editing-drift items to merge.", "info");
		return;
	}
	const dispatcher = await tryLoadScaffoldKeeper();
	if (!dispatcher) {
		ctx.ui.notify(
			"--include-editing: scaffold-keeper not yet available (ADR-021 Phase D pending).",
			"warning",
		);
		return;
	}
	try {
		// Phase D's dispatcher emits its own `approval_request` notification when
		// it writes a .proposed file; we additionally print the per-path summary
		// the brief specifies for the explicit `--include-editing` invocation.
		const written = await dispatcher(basePath, ctx);
		if (written === 0) {
			ctx.ui.notify(
				"scaffold-keeper completed without producing .proposed files.",
				"info",
			);
			return;
		}
		const lines = [
			`Wrote ${written} .proposed file${written === 1 ? "" : "s"}:`,
			...editingItems.map((i) => `  ${i.path}.proposed`),
		];
		ctx.ui.notify(lines.join("\n"), "info");
	} catch (err) {
		ctx.ui.notify(`scaffold-keeper failed: ${err.message}`, "warning");
	}
}
