import { resolveModelId } from "./auto-model-selection.js";
import { canonicalIdFor, sameGeneration } from "./model-registry.js";
import { logWarning } from "./workflow-logger.js";

// ── Solver pinning (ADR-0079) ────────────────────────────────────────────────
// The autonomous solver pass is always locked to kimi-k2.6 (provider:
// kimi-coding) and must never cross canonical_id boundaries on failover.
// The unit type string "autonomous-solver" is the identifier introduced by
// ADR-0079 for the solver role. Other unit types run as executor and may
// cross canonical ids (with a structured downgrade log event).
const SOLVER_PINNED_UNIT_TYPE = "autonomous-solver";

/**
 * Emit a structured log event when a failover crosses a canonical-id or
 * generation boundary.  Written to workflow-logger so it flows through the
 * audit log and drainAndSummarize() for post-mortem analysis.
 *
 * @param {string} fromCanonical  - canonical id of the route that failed
 * @param {string} toCanonical    - canonical id of the chosen failover route
 * @param {string} unitType       - active unit type at failover time
 * @param {string} reason         - human-readable reason label
 */
export function logGenerationDowngrade(
	fromCanonical,
	toCanonical,
	unitType,
	reason,
) {
	logWarning("model-route-failure", "generation-downgrade", {
		from: fromCanonical,
		to: toCanonical,
		unitType,
		reason: reason ?? "cross-generation failover",
		sameGeneration: false,
	});
}
/**
 * Build the stable identity key for a concrete provider route.
 *
 * Purpose: make fallback recovery compare full provider/model routes instead of
 * ambiguous bare model ids.
 *
 * Consumer: resolveNextConfiguredModelRoute() when skipping failed and current
 * runtime routes.
 */
export function modelRouteKey(route) {
	return `${route.provider.toLowerCase()}/${route.id.toLowerCase()}`;
}
function dedupeConfiguredRoutes(modelConfig) {
	const seen = new Set();
	const routes = [];
	for (const route of [modelConfig.primary, ...modelConfig.fallbacks]) {
		const key = route.toLowerCase();
		if (seen.has(key)) continue;
		seen.add(key);
		routes.push(route);
	}
	return routes;
}
/**
 * Resolve the next configured model route after a provider/model failure.
 *
 * Purpose: keep autonomous mode recovery inside the user's explicit primary/fallback
 * chain, skip routes already failed for this unit, and avoid returning the same
 * provider/model again.
 *
 * Consumer: bootstrap/agent-end-recovery.ts when a provider returns quota,
 * rate-limit, server, stream, or connection failures during a unit.
 */
export function resolveNextConfiguredModelRoute(args) {
	const routes = dedupeConfiguredRoutes(args.modelConfig);
	const currentKey = args.current ? modelRouteKey(args.current) : undefined;
	const failedKeys = new Set(
		args.failedRoutes.map((failure) =>
			modelRouteKey({ provider: failure.provider, id: failure.modelId }),
		),
	);
	const resolvedRoutes = routes.map((configuredRoute) => ({
		configuredRoute,
		model: resolveModelId(
			configuredRoute,
			args.availableModels,
			args.current?.provider,
		),
	}));
	const currentIndex =
		currentKey === undefined
			? -1
			: resolvedRoutes.findIndex(
					(route) => route.model && modelRouteKey(route.model) === currentKey,
				);
	const candidates =
		currentIndex >= 0 ? resolvedRoutes.slice(currentIndex + 1) : resolvedRoutes;
	for (const candidate of candidates) {
		if (!candidate.model) continue;
		const candidateKey = modelRouteKey(candidate.model);
		if (candidateKey === currentKey) continue;
		if (failedKeys.has(candidateKey)) continue;
		if (args.isBlocked?.(candidate.model)) continue;
		return {
			model: candidate.model,
			route: candidate.configuredRoute,
			source: "configured",
		};
	}
	return undefined;
}
/**
 * Resolve another currently available provider/model route when configured
 * fallbacks are missing or exhausted.
 *
 * Purpose: keep autonomous mode moving on quota/rate-limit/server failures instead
 * of pausing just because the configured fallback chain did not cover every
 * live provider route.
 *
 * Consumer: bootstrap/agent-end-recovery.ts after configured fallback lookup
 * fails for a model-route failure.
 *
 * Generation guard (ADR-0079):
 *   - If unitType is "autonomous-solver" (solver-pinned), candidates whose
 *     canonical_id differs from the failed route are silently skipped. The
 *     solver layer is a runtime invariant and must never silently degrade to a
 *     different model generation.
 *   - For all other unit types (executor layer), cross-canonical failover is
 *     permitted but emits a structured generation-downgrade log event so it is
 *     visible in traces and drainAndSummarize() audits.
 */
export function resolveNextAvailableModelRoute(args) {
	const currentKey = args.current ? modelRouteKey(args.current) : undefined;
	const currentRouteKey = args.current
		? `${args.current.provider}/${args.current.id}`
		: undefined;
	const currentCanonical = currentRouteKey
		? canonicalIdFor(currentRouteKey)
		: null;
	const isSolverPinned = args.unitType === SOLVER_PINNED_UNIT_TYPE;

	const failedKeys = new Set(
		args.failedRoutes.map((failure) =>
			modelRouteKey({ provider: failure.provider, id: failure.modelId }),
		),
	);
	const candidates = args.availableModels.filter((model) => {
		const key = modelRouteKey(model);
		if (key === currentKey) return false;
		if (failedKeys.has(key)) return false;
		if (args.isBlocked?.(model)) return false;

		// Solver pin: ADR-0079 — never cross canonical_id boundary when solving.
		if (isSolverPinned && currentCanonical !== null) {
			const candidateRouteKey = `${model.provider}/${model.id}`;
			const candidateCanonical = canonicalIdFor(candidateRouteKey);
			if (candidateCanonical !== currentCanonical) return false;
		}

		return true;
	});
	if (candidates.length === 0) return undefined;
	const differentProvider =
		args.current &&
		candidates.find(
			(model) =>
				model.provider.toLowerCase() !== args.current.provider.toLowerCase(),
		);
	const model = differentProvider ?? candidates[0];

	// Generation guard: log a structured event when crossing canonical_id or
	// generation boundaries on the executor layer (non-solver-pinned).
	if (!isSolverPinned && currentCanonical !== null) {
		const chosenRouteKey = `${model.provider}/${model.id}`;
		const chosenCanonical = canonicalIdFor(chosenRouteKey);
		if (
			chosenCanonical !== null &&
			!sameGeneration(currentCanonical, chosenCanonical)
		) {
			logGenerationDowngrade(
				currentCanonical,
				chosenCanonical,
				args.unitType ?? "unknown",
				"no same-generation route available",
			);
		}
	}

	return {
		model,
		route: `${model.provider}/${model.id}`,
		source: "available",
	};
}
/**
 * Resolve the next model route by trying configured policy first, then any
 * other live route.
 *
 * Purpose: preserve configured fallback ordering when it exists while still
 * enforcing the no-pause contract for transient provider/model failures.
 *
 * Consumer: bootstrap/agent-end-recovery.ts during provider-route recovery.
 */
export function resolveNextModelRoute(args) {
	if (args.modelConfig) {
		const configured = resolveNextConfiguredModelRoute({
			current: args.current,
			modelConfig: args.modelConfig,
			availableModels: args.availableModels,
			failedRoutes: args.failedRoutes,
			isBlocked: args.isBlocked,
		});
		if (configured) return configured;
	}
	return resolveNextAvailableModelRoute({
		current: args.current,
		availableModels: args.availableModels,
		failedRoutes: args.failedRoutes,
		isBlocked: args.isBlocked,
		unitType: args.unitType,
	});
}
