import { ARTIFACT_CREATE_PREFIX } from "../artifacts/artifact-component-schema.js";
import { ArtifactParser } from "../artifacts/ArtifactParser.js";
import { ContractViolationError } from "../errors/contract-violation-error.js";
import { TRANSFER_TOOL_PREFIX } from "@inkeep/agents-core";

//#region src/domains/run/agents/output-contract.ts
function resolveAllowText(outputContract) {
	return outputContract?.allowText ?? true;
}
function deriveContractEnforcement(params) {
	const { outputContract, resolvedAllowText, hasStructuredOutput } = params;
	return hasStructuredOutput || resolvedAllowText === false || (outputContract?.requireComponent?.length ?? 0) > 0 || (outputContract?.requireArtifact?.length ?? 0) > 0 || outputContract?.requireTransfer === true;
}
function resolveContractToolChoice(params) {
	const { resolvedAllowText, hasStructuredOutput, hasArtifactComponents } = params;
	return resolvedAllowText === false && !hasStructuredOutput && !hasArtifactComponents ? "required" : "auto";
}
/**
* Returns a human-readable description of the first contract rule the response
* violates, or null when the contract is satisfied. The description names the
* offending rule and what was expected vs. emitted, so the failure is
* diagnosable from the error/trace alone.
*/
function getContractViolation(params) {
	const { ctx, response, hasStructuredOutput } = params;
	const contract = ctx.config.outputContract;
	if (!contract) return null;
	const lastStepToolCalls = (response.steps?.at(-1))?.toolCalls ?? [];
	const componentNames = response.output?.dataComponents?.map((dc) => dc?.name) ?? [];
	const emitted = componentNames.filter(Boolean).join(", ") || "none";
	if (ctx.resolvedAllowText === false) {
		const producedStructured = hasStructuredOutput && Boolean(response.output);
		const producedToolCall = lastStepToolCalls.length > 0;
		if (!producedStructured && !producedToolCall) return "free text is disallowed (allowText: false) but the turn produced no data component, artifact, transfer, or tool call";
	}
	const missingComponents = (contract.requireComponent ?? []).filter((name) => !componentNames.includes(name));
	if (missingComponents.length > 0) return `requireComponent — the response must include data component(s) [${missingComponents.join(", ")}] but emitted [${emitted}]`;
	const responseText = response.text ?? "";
	const artifactsById = ctx.config.artifacts ?? {};
	const presentArtifactTypes = new Set(ArtifactParser.parseCreateAnnotations(responseText).map((annotation) => annotation.type));
	const addReferencedType = (artifactId) => {
		const type = typeof artifactId === "string" ? artifactsById[artifactId]?.type : void 0;
		if (type) presentArtifactTypes.add(type);
	};
	for (const refId of ArtifactParser.parseRefIds(responseText)) addReferencedType(refId);
	for (const dc of response.output?.dataComponents ?? []) if (dc?.name === "Artifact") addReferencedType(dc?.props?.artifact_id);
	const missingArtifacts = (contract.requireArtifact ?? []).filter((name) => !componentNames.includes(`${ARTIFACT_CREATE_PREFIX}${name}`) && !presentArtifactTypes.has(name));
	if (missingArtifacts.length > 0) {
		const createdViaDataComponent = componentNames.flatMap((n) => n?.startsWith(ARTIFACT_CREATE_PREFIX) ? [n.slice(ARTIFACT_CREATE_PREFIX.length)] : []);
		const present = [...new Set([...createdViaDataComponent, ...presentArtifactTypes])].join(", ") || "none";
		return `requireArtifact — the response must create or reference artifact(s) [${missingArtifacts.join(", ")}] but produced [${present}]`;
	}
	if (contract.requireTransfer === true && !lastStepToolCalls.some((toolCall) => toolCall?.toolName?.startsWith(TRANSFER_TOOL_PREFIX))) return "requireTransfer — the response must transfer to another sub agent, but no transfer occurred";
	return null;
}
/** Boolean form of {@link getContractViolation}. */
function isContractViolation(params) {
	return getContractViolation(params) !== null;
}
function enforceOutputContract(params) {
	const { ctx, response, hasStructuredOutput, hasContractEnforcement, textResponse, span, logger } = params;
	const contract = ctx.config.outputContract;
	if (!hasContractEnforcement || !contract) return;
	const configuredPolicy = contract.onViolation ?? "retry";
	const violationReason = getContractViolation({
		ctx,
		response,
		hasStructuredOutput
	});
	const violated = violationReason !== null;
	const effectivePolicy = configuredPolicy === "warn" ? "warn" : "reject";
	span.setAttributes({
		"agent.output_contract.violated": violated,
		"agent.output_contract.policy_applied": violated ? effectivePolicy : "none",
		...violationReason ? { "agent.output_contract.violation_reason": violationReason } : {}
	});
	if (!violationReason) return;
	if (effectivePolicy === "warn") {
		logger.warn({
			agentId: ctx.config.id,
			conversationId: ctx.conversationId,
			policy: configuredPolicy,
			reason: violationReason
		}, "Output contract violated; onViolation=warn, surfacing fallback text");
		return;
	}
	throw new ContractViolationError({
		subAgentId: ctx.config.id,
		policy: effectivePolicy,
		attemptedRetries: 0,
		lastResponseText: textResponse,
		reason: violationReason
	});
}

//#endregion
export { deriveContractEnforcement, enforceOutputContract, getContractViolation, isContractViolation, resolveAllowText, resolveContractToolChoice };