/**
 * model-role-policy.js -- role intent projection for autonomous model routing.
 *
 * Purpose: preserve durable SF model-routing policy as role-level intent and
 * symbolic constraints so auto model selection remains the only component that
 * resolves concrete provider/model IDs.
 */

/**
 * List the autonomous roles that can carry durable model-routing intent.
 *
 * Purpose: keep role-policy validation aligned with SF's autonomous dispatch
 * vocabulary instead of allowing arbitrary role names to become configuration.
 *
 * Consumer: preference/import projection code before passing policy intent to
 * autonomous model selection.
 */
export const SUPPORTED_MODEL_ROLES = Object.freeze([
	"orchestrator",
	"worker",
	"validator",
	"reviewer",
]);

/**
 * List symbolic constraints that role policy can persist.
 *
 * Purpose: allow durable policy to express routing needs without persisting
 * provider names, model names, or custom model IDs.
 *
 * Consumer: role-policy validation and autonomous route evidence writers.
 */
export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
	"coding",
	"review",
	"cheap",
	"long-context",
	"byok-allowed",
	"local-only",
	"strict-json",
]);

const MODEL_ROLES = new Set(SUPPORTED_MODEL_ROLES);
const MODEL_ROLE_CONSTRAINTS = new Set(SUPPORTED_MODEL_ROLE_CONSTRAINTS);
const CONCRETE_ROUTE_KEYS = new Set([
	"provider",
	"providerId",
	"modelId",
	"model_id",
	"customModelId",
	"custom_model_id",
]);

/**
 * Provide SF's default symbolic routing constraints per autonomous role.
 *
 * Purpose: encode the useful Droid-style role projection while leaving final
 * provider/model choice to SF's existing auto model selection.
 *
 * Consumer: normalizeRolePolicy when a role has no explicit constraints.
 */
export const DEFAULT_MODEL_ROLE_CONSTRAINTS = Object.freeze({
	orchestrator: Object.freeze(["long-context", "strict-json"]),
	worker: Object.freeze(["coding"]),
	validator: Object.freeze(["strict-json", "review"]),
	reviewer: Object.freeze(["review"]),
});

/**
 * Report invalid durable model-role policy input.
 *
 * Purpose: give preference and projection callers one clear error type when a
 * role policy tries to persist unsupported roles, constraints, or route IDs.
 *
 * Consumer: normalizeRolePolicy and normalizeRolePolicies callers that surface
 * validation failures to users or startup diagnostics.
 */
export class ModelRolePolicyValidationError extends Error {
	constructor(errors) {
		super(`Invalid model role policy: ${errors.join("; ")}`);
		this.name = "ModelRolePolicyValidationError";
		this.errors = errors;
	}
}

function normalizeRoleName(role) {
	return String(role ?? "")
		.trim()
		.toLowerCase();
}

function normalizeConstraintName(constraint) {
	return String(constraint ?? "")
		.trim()
		.toLowerCase()
		.replaceAll("_", "-");
}

function constraintsFromPolicy(role, policy) {
	if (Array.isArray(policy)) return policy;
	if (
		policy &&
		typeof policy === "object" &&
		Array.isArray(policy.constraints)
	) {
		return policy.constraints;
	}
	return DEFAULT_MODEL_ROLE_CONSTRAINTS[role] ?? [];
}

function modeFromPolicy(policy) {
	if (policy === undefined || policy === null) return "auto";
	if (policy === "auto") return "auto";
	if (Array.isArray(policy)) return "auto";
	if (policy && typeof policy === "object") {
		return policy.mode ?? policy.model ?? "auto";
	}
	return policy;
}

function validateOne(role, policy) {
	const normalizedRole = normalizeRoleName(role);
	const errors = [];
	if (!MODEL_ROLES.has(normalizedRole)) {
		errors.push(
			`unsupported model role "${String(role)}"; expected one of ${SUPPORTED_MODEL_ROLES.join(", ")}`,
		);
	}
	if (policy && typeof policy === "object" && !Array.isArray(policy)) {
		for (const key of Object.keys(policy)) {
			if (CONCRETE_ROUTE_KEYS.has(key)) {
				errors.push(
					`role "${normalizedRole}" must use mode "auto"; durable policy cannot include "${key}"`,
				);
			}
		}
	}
	const mode = modeFromPolicy(policy);
	if (mode !== "auto") {
		errors.push(
			`role "${normalizedRole}" must use mode "auto"; received "${String(mode)}"`,
		);
	}
	const seen = new Set();
	const constraints = [];
	for (const rawConstraint of constraintsFromPolicy(normalizedRole, policy)) {
		const constraint = normalizeConstraintName(rawConstraint);
		if (!MODEL_ROLE_CONSTRAINTS.has(constraint)) {
			errors.push(
				`role "${normalizedRole}" has unsupported constraint "${String(rawConstraint)}"; expected one of ${SUPPORTED_MODEL_ROLE_CONSTRAINTS.join(", ")}`,
			);
			continue;
		}
		if (!seen.has(constraint)) {
			seen.add(constraint);
			constraints.push(constraint);
		}
	}
	return {
		ok: errors.length === 0,
		errors,
		policy:
			errors.length === 0
				? { role: normalizedRole, mode: "auto", constraints }
				: null,
	};
}

/**
 * Validate one durable autonomous model role policy.
 *
 * Purpose: let callers reject provider-specific routing data before it reaches
 * SF's durable preference or planning state.
 *
 * Consumer: preference projection, Droid-pattern importers, and tests that need
 * non-throwing diagnostics.
 */
export function validateRolePolicy(role, policy = undefined) {
	return validateOne(role, policy);
}

/**
 * Normalize one durable autonomous model role policy.
 *
 * Purpose: produce the canonical role-policy shape consumed by autonomous model
 * selection while guaranteeing concrete route IDs are not persisted.
 *
 * Consumer: startup preference loading and any future projection step that feeds
 * role intent into UOK model route evidence.
 */
export function normalizeRolePolicy(role, policy = undefined) {
	const result = validateOne(role, policy);
	if (!result.ok) throw new ModelRolePolicyValidationError(result.errors);
	return result.policy;
}

/**
 * Normalize a role-to-policy map for autonomous model routing.
 *
 * Purpose: project partial durable policy into a complete role map where every
 * supported autonomous role requests SF's existing auto model selection.
 *
 * Consumer: SF-native policy import/projection code before dispatch starts an
 * autonomous run.
 */
export function normalizeRolePolicies(policies = {}) {
	if (!policies || typeof policies !== "object" || Array.isArray(policies)) {
		throw new ModelRolePolicyValidationError([
			"model role policies must be an object keyed by role",
		]);
	}
	const unknownRoles = Object.keys(policies).filter(
		(role) => !MODEL_ROLES.has(normalizeRoleName(role)),
	);
	if (unknownRoles.length > 0) {
		throw new ModelRolePolicyValidationError(
			unknownRoles.map(
				(role) =>
					`unsupported model role "${role}"; expected one of ${SUPPORTED_MODEL_ROLES.join(", ")}`,
			),
		);
	}
	const normalizedPolicies = Object.fromEntries(
		Object.entries(policies).map(([role, policy]) => [
			normalizeRoleName(role),
			policy,
		]),
	);
	return Object.fromEntries(
		SUPPORTED_MODEL_ROLES.map((role) => [
			role,
			normalizeRolePolicy(role, normalizedPolicies[role]),
		]),
	);
}
