/**
 * profile/resolve.mjs — Cascade walk for the single-parent `extends` chain.
 *
 * Algorithm:
 *   1. Load the target profile via loadProfile; on failure, propagate.
 *   2. Walk the `extends` chain upward, collecting `chain = [root, …, target]`.
 *      Guard against cycles (visited Set) and exceeding MAX_PROFILE_DEPTH.
 *      A dangling `extends: <name>` where the parent profile doesn't exist is
 *      a hard fail.
 *   3. Fold the chain left-to-right. For each optional field (obligations,
 *      conventions, tooling), the default behavior is `replace` (child wins
 *      entirely). Top-level `merge_*: true` sentinels on the child flip that
 *      field into merge mode:
 *        - obligations → concat + dedup by `id` (child wins on collision)
 *        - conventions, tooling → shallow object merge (child wins per key)
 *      The final `name`, `extends`, and `description` are taken from the
 *      target (last) profile. The `merge_*` sentinels are stripped from the
 *      resolved output.
 *
 * Constants:
 *   MAX_PROFILE_DEPTH = 8  — hard cap on chain length.
 */

import { loadProfile } from './parse.mjs'

export const MAX_PROFILE_DEPTH = 8

/**
 * Resolve the cascade chain for a named profile.
 *
 * @param {string} projectRoot
 * @param {string} name target profile slug
 * @returns {{ok: true, chain: string[], resolved: Record<string, any>, path: string|undefined}
 *   | {ok: false, reason: string, chain?: string[], errors?: object[]}}
 */
export function resolveProfile(projectRoot, name) {
	// Walk from target → parents, prepending each one so the final chain is
	// ordered [root, …, target]. `visited` guards against cycles; the chain
	// length guards against accidentally-deep inheritance trees.
	const loaded = new Map() // name -> profile
	const chain = []
	const visited = new Set()
	let cursor = name
	while (cursor) {
		if (visited.has(cursor)) {
			const cyclePath = [...chain, cursor]
			return {
				ok: false,
				reason: `cycle detected: ${cyclePath.join(' → ')}`,
				chain: cyclePath,
			}
		}
		visited.add(cursor)
		const result = loadProfile(projectRoot, cursor)
		if (!result.ok) {
			// The root-most missing profile is reported as "unknown parent" when
			// we got here via an `extends` pointer; as "profile not found" when
			// the user directly requested it.
			const isParent = chain.length > 0
			const reason = isParent ? `unknown parent profile: ${cursor}` : `profile not found: ${cursor}`
			return { ok: false, reason, errors: result.errors }
		}
		loaded.set(cursor, result.profile)
		chain.unshift(cursor)
		if (chain.length > MAX_PROFILE_DEPTH) {
			return {
				ok: false,
				reason: `max profile depth exceeded (${MAX_PROFILE_DEPTH})`,
				chain,
			}
		}
		cursor = result.profile.extends
	}

	// Fold left-to-right — the acc starts as the root profile and each child
	// either replaces or merges into each mergeable field.
	let acc = stripSentinels(loaded.get(chain[0]))
	for (let i = 1; i < chain.length; i++) {
		const childRaw = loaded.get(chain[i])
		acc = foldChild(acc, childRaw)
	}
	// The final identity (name/extends/description) comes from the target.
	const target = loaded.get(chain[chain.length - 1])
	acc.name = target.name
	if (target.extends !== undefined) acc.extends = target.extends
	else delete acc.extends
	if (target.description !== undefined) acc.description = target.description
	else delete acc.description

	return {
		ok: true,
		chain,
		resolved: acc,
		path: undefined,
	}
}

/**
 * Return a shallow copy of the profile with the merge_* sentinels removed.
 * The resolved output should not leak these fold-control flags.
 */
function stripSentinels(profile) {
	const { merge_obligations, merge_conventions, merge_tooling, ...rest } = profile
	return { ...rest }
}

/**
 * Fold one child profile into the accumulator.
 *
 *   - obligations: if child.merge_obligations → concat + dedup by id (child
 *     wins); else child replaces acc's obligations (if child omits the key,
 *     acc keeps its value — only an explicit child value overrides).
 *   - conventions: if child.merge_conventions → shallow merge (child wins per
 *     key); else child replaces acc's value when present.
 *   - tooling: same as conventions with merge_tooling.
 *
 * The `replace on explicit` rule matters because the fold is sequential: a
 * 3-level chain `default → team → feature` lets `team` override `default`'s
 * obligations even when `feature` omits them.
 */
function foldChild(acc, child) {
	const next = { ...acc }

	// Obligations
	if (Object.hasOwn(child, 'obligations')) {
		if (child.merge_obligations === true) {
			const byId = new Map()
			for (const o of acc.obligations ?? []) byId.set(o.id, o)
			for (const o of child.obligations ?? []) byId.set(o.id, o) // child wins
			next.obligations = [...byId.values()]
		} else {
			next.obligations = child.obligations
		}
	}

	// Conventions
	if (Object.hasOwn(child, 'conventions')) {
		if (child.merge_conventions === true) {
			next.conventions = { ...(acc.conventions ?? {}), ...child.conventions }
		} else {
			next.conventions = child.conventions
		}
	}

	// Tooling
	if (Object.hasOwn(child, 'tooling')) {
		if (child.merge_tooling === true) {
			next.tooling = { ...(acc.tooling ?? {}), ...child.tooling }
		} else {
			next.tooling = child.tooling
		}
	}

	return next
}
