// SF — UnitContextComposer (#4782 phase 2).
//
// Reads a unit type's manifest and orchestrates artifact inlining through
// a caller-provided resolver. Returns a joined context block suitable for
// substitution into the unit's prompt template.
//
// Design rationale:
//   - Pure dependency on the manifest module — no circular import with
//     `auto-prompts.ts` where the per-artifact-key resolver lives.
//   - Caller-supplied resolver means the composer can be unit-tested with
//     trivial mocks; production wiring in `auto-prompts.ts` dispatches to
//     the existing `inlineFile` / `inline*FromDb` helpers.
//   - Null-returning resolvers are skipped silently: they model the
//     "artifact is optional / missing / not applicable to this milestone"
//     case. The composer never errors on a missing artifact.
//
// Scope: phase 2 pilot shipped `composeInlinedContext` for static-key
// inlining. Phase 3.5 (#4924) adds the v2 surface — `composeUnitContext`
// — which also handles excerpts, computed artifacts, and prepended blocks.
// `composeInlinedContext` stays for backward compatibility with the
// already-migrated simple builders.
//
// ─── Composer boundary invariant (#4924) ─────────────────────────────────
//
// The composer is allowed to:
//   - order named sections per the manifest's declared sequence
//   - resolve registered artifacts (static / computed / excerpt / on-demand)
//   - apply typed policies (knowledge / memory / codebase-map / preferences)
//
// The composer must NOT grow:
//   - arbitrary conditionals on unit state
//   - loops over caller-supplied data
//   - string templating beyond section composition (join + separator)
//
// Logic that needs those belongs in a typed computed-artifact builder
// owned by the unit, not in the composer. Reviews must enforce this — it
// is the difference between an orchestrator and a runaway DSL.
import { resolveManifest } from "./unit-context-manifest.js";
/**
 * Produce the inlined-context portion of a unit's system prompt by
 * walking the manifest's `artifacts.inline` list in order and calling
 * the provided resolver for each key.
 *
 * Returns an empty string when the unit type has no manifest registered,
 * so callers can guard their wiring with a simple truthy check. Unknown
 * unit types do not error — this mirrors `resolveManifest`'s contract.
 *
 * The separator between inlined blocks matches the in-tree convention
 * (`\n\n---\n\n`) so composer output slots into existing prompt templates
 * without visible diff.
 */
export async function composeInlinedContext(unitType, resolveArtifact) {
	const manifest = resolveManifest(unitType);
	if (!manifest) return "";
	// Resolve all artifact keys in parallel — each resolver is an independent
	// DB read or file load, so sequential awaiting just serializes I/O for no gain.
	const results = await Promise.all(
		manifest.artifacts.inline.map(resolveArtifact),
	);
	const blocks = results.filter((b) => b !== null && b.length > 0);
	return blocks.join("\n\n---\n\n");
}
/**
 * Convenience helper returning the manifest's declared budget so callers
 * can telemetry a mismatch between actual prompt size and declared budget.
 * Returns null for unknown unit types.
 */
export function manifestBudgetChars(unitType) {
	const manifest = resolveManifest(unitType);
	return manifest ? manifest.maxSystemPromptChars : null;
}
const SECTION_SEPARATOR = "\n\n---\n\n";
/**
 * Compose all manifest-declared context for a unit type using the v2
 * surface. Walks `prepend` first (computed-only), then the `inline` list
 * (static keys via `resolveArtifact`), then `excerpt` (via `resolveExcerpt`),
 * then `artifacts.computed` (via the typed registry). Order within each
 * section follows the manifest's declared sequence.
 *
 * Unknown unit types return empty strings for both sections — callers can
 * fall back to existing imperative wiring without a special case.
 *
 * Resolver / registry omissions: if the manifest declares an entry but no
 * resolver / registry entry is provided, the composer skips it silently.
 * This matches the v1 contract where a null body is a no-op, and lets
 * partial migrations land without forcing every consumer to register
 * every artifact class up-front.
 */
export async function composeUnitContext(unitType, opts) {
	const manifest = resolveManifest(unitType);
	if (!manifest) return { prepend: "", inline: "" };
	// Resolve prepend computed artifacts, inline keys, excerpt keys, and inline
	// computed artifacts all in parallel — they are independent I/O operations.
	const [prependBlocks, inlineResolved, excerptResolved, computedBlocks] =
		await Promise.all([
			runComputed(manifest.prepend ?? [], opts),
			opts.resolveArtifact
				? Promise.all(manifest.artifacts.inline.map(opts.resolveArtifact))
				: Promise.resolve([]),
			opts.resolveExcerpt
				? Promise.all(manifest.artifacts.excerpt.map(opts.resolveExcerpt))
				: Promise.resolve([]),
			runComputed(manifest.artifacts.computed ?? [], opts),
		]);
	const inlineBlocks = [
		...inlineResolved.filter((b) => b != null && b.length > 0),
		...excerptResolved.filter((b) => b != null && b.length > 0),
		...computedBlocks,
	];
	return {
		prepend: prependBlocks.join(SECTION_SEPARATOR),
		inline: inlineBlocks.join(SECTION_SEPARATOR),
	};
}
/**
 * Invoke the registered builder for each declared computed id, in order.
 * Missing registry entries (manifest declares the id but caller didn't
 * register it) are skipped silently — see composeUnitContext rationale.
 */
async function runComputed(ids, opts) {
	if (ids.length === 0 || !opts.computed) return [];
	// Computed builders are independent — run them in parallel and filter
	// nulls/empties while preserving the manifest's declared order.
	const results = await Promise.all(
		ids.map(async (id) => {
			const entry = opts.computed[id];
			if (!entry) return null;
			return entry.build(entry.inputs, opts.base);
		}),
	);
	return results.filter((b) => b != null && b.length > 0);
}
