/**
 * Preconfigured Workflow Builder Agent Tool
 *
 * Creates a focused sub-agent that writes TypeScript SDK code and validates it.
 * Two modes:
 * - Sandbox mode (when workspace is available): agent works with real files + tsc
 * - Tool mode (fallback): agent uses build-workflow tool with string-based code
 */

import { Agent, Tool, type BuiltTool, type RuntimeSkillSource, type Workspace } from '@n8n/agents';
import { generateWorkflowCode } from '@n8n/workflow-sdk';
import { UserError } from 'n8n-workflow';
import { nanoid } from 'nanoid';
import { createHash, randomUUID } from 'node:crypto';
import { z } from 'zod';

import { createSubAgentPersistence, createSubAgentResourceId } from './agent-persistence';
import {
	BUILDER_AGENT_PROMPT,
	createSandboxBuilderAgentPrompt,
} from './build-workflow-agent.prompt';
import { compactBuilderMemoryThread } from './builder-memory-compaction';
import { truncateLabel } from './display-utils';
import {
	createDetachedSubAgentTraceFactory,
	traceSubAgentTools,
	withTraceContextActor,
} from './tracing-utils';
import { createVerifyBuiltWorkflowTool } from './verify-built-workflow.tool';
import { attachRuntimeWorkspaceCapabilities } from '../../agent/runtime-workspace';
import { buildSubAgentBriefing } from '../../agent/sub-agent-briefing';
import { MAX_STEPS } from '../../constants/max-steps';
import type { Logger } from '../../logger';
import {
	createPrebakedRuntimeSkillsFromWorkspace,
	materializeRuntimeSkillsIntoWorkspace,
	type MaterializedRuntimeSkills,
} from '../../skills/materialize-runtime-skills';
import { hasRuntimeSkills } from '../../skills/runtime-skills';
import { consumeStreamWithHitl, requireCompletedHitlText } from '../../stream/consume-with-hitl';
import { createToolRegistry, toolRegistryKeys, toolRegistryValues } from '../../tool-registry';
import { buildAgentTraceInputs, mergeTraceRunInputs } from '../../tracing/langsmith-tracing';
import type {
	BackgroundTaskResult,
	InstanceAiContext,
	InstanceAiToolRegistry,
	OrchestrationContext,
} from '../../types';
import { SDK_IMPORT_STATEMENT } from '../../workflow-builder/extract-code';
import {
	createRemediation,
	type RemediationMetadata,
	type TriggerType,
	type WorkflowBuildOutcome,
	type WorkflowSetupRequirement,
	type WorkflowVerificationReadiness,
	type WorkflowLoopState,
} from '../../workflow-loop';
import {
	readFileViaSandbox,
	writeFileViaSandbox,
	type SandboxWorkspace,
} from '../../workspace/sandbox-fs';
import { getWorkspaceRoot } from '../../workspace/sandbox-setup';
import { createScopedWorkspace } from '../../workspace/scoped-workspace';
import {
	attachTemplateTelemetrySession,
	createTemplateTelemetrySession,
	createTypedToolObserver,
	detachTemplateTelemetrySession,
	type TemplateTelemetrySession,
} from '../../workspace/template-telemetry';
import {
	CREDENTIALS_TOOL_ID,
	createCredentialsTool,
	type CredentialAction,
} from '../credentials.tool';
import { DATA_TABLES_TOOL_ID } from '../data-tables.tool';
import { ASK_USER_TOOL_ID } from '../shared/ask-user.tool';
import { buildCredentialMap, type CredentialMap } from '../workflows/resolve-credentials';
import { createIdentityEnforcedSubmitWorkflowTool } from '../workflows/submit-workflow-identity';
import {
	type SubmitWorkflowAttempt,
	type SubmitWorkflowOutput,
} from '../workflows/submit-workflow.tool';
import { isMockableTriggerNodeType } from '../workflows/workflow-json-utils';
import { createWorkflowsTool, type WorkflowAction } from '../workflows.tool';

interface BuilderMemoryBinding {
	resource: string;
	thread: string;
}

export interface BuildWorkflowAgentRunResult {
	text: string;
	outcome: WorkflowBuildOutcome;
}

export interface MainWorkflowSnapshot {
	exists: boolean;
	sourceHash?: string;
}

const WORKFLOW_NOT_SUBMITTED_FAILURE_SIGNATURE = 'workflow:not_submitted';
const WORKFLOW_FINAL_SUBMIT_FAILED_FAILURE_SIGNATURE = 'workflow:final_submit_failed';

export function getBuilderSessionMemory(
	context: Pick<OrchestrationContext, 'memory'>,
	useSharedWorkspace: boolean,
): OrchestrationContext['memory'] {
	return useSharedWorkspace ? context.memory : undefined;
}

const BUILDER_WORK_ITEMS_DIR = 'builder-work-items';

export interface BuilderWorkflowWorkspaceLayout {
	workItemRoot: string;
	sourceDir: string;
	chunksDir: string;
	mainWorkflowPath: string;
	tsconfigPath: string;
	relativeMainWorkflowPath: string;
}

function safeWorkItemPathSegment(workItemId: string): string {
	const slug = workItemId
		.replace(/[^A-Za-z0-9_-]+/g, '-')
		.replace(/^-+|-+$/g, '')
		.slice(0, 48);
	const hash = createHash('sha256').update(workItemId).digest('hex').slice(0, 8);

	return `${slug || 'work-item'}-${hash}`;
}

export function builderWorkflowWorkspaceLayout(
	root: string,
	workItemId: string,
): BuilderWorkflowWorkspaceLayout {
	const relativeWorkItemRoot = `${BUILDER_WORK_ITEMS_DIR}/${safeWorkItemPathSegment(workItemId)}`;
	const workItemRoot = `${root}/${relativeWorkItemRoot}`;

	return {
		workItemRoot,
		sourceDir: `${workItemRoot}/src`,
		chunksDir: `${workItemRoot}/chunks`,
		mainWorkflowPath: `${workItemRoot}/src/workflow.ts`,
		tsconfigPath: `${workItemRoot}/tsconfig.json`,
		relativeMainWorkflowPath: `${relativeWorkItemRoot}/src/workflow.ts`,
	};
}

function renderBuilderTaskTsconfig(): string {
	return `${JSON.stringify(
		{
			extends: '../../tsconfig.json',
			include: ['src/**/*.ts', 'chunks/**/*.ts'],
		},
		null,
		2,
	)}\n`;
}

async function writeBuilderWorkspaceFile(
	workspace: SandboxWorkspace,
	filePath: string,
	content: string,
): Promise<void> {
	if (workspace.filesystem) {
		await workspace.filesystem.writeFile(filePath, content, { recursive: true });
		return;
	}

	await writeFileViaSandbox(workspace, filePath, content);
}

export async function materializeBuilderRuntimeSkills(
	context: OrchestrationContext,
	workspace: Workspace,
	root: string,
): Promise<{ workspace: Workspace; source?: RuntimeSkillSource }> {
	const source = context.runtimeSkillCatalog ?? context.runtimeSkills;
	if (!hasRuntimeSkills(source)) {
		return { workspace, source };
	}

	let materialized: MaterializedRuntimeSkills | undefined;
	try {
		const workspaceRoot = await getWorkspaceRoot(workspace);
		materialized = await createPrebakedRuntimeSkillsFromWorkspace({
			source,
			workspace,
			root: workspaceRoot,
			workspaceRoot: root,
			logger: context.logger,
		});
	} catch (error) {
		context.logger.debug('Could not inspect prebaked runtime skills; materializing live', {
			error: error instanceof Error ? error.message : String(error),
		});
	}

	materialized ??= await materializeRuntimeSkillsIntoWorkspace({
		source,
		workspace,
		root,
		logger: context.logger,
	});

	if (!materialized) return { workspace, source };

	return {
		workspace: createScopedWorkspace(workspace, root, materialized.env),
		source: materialized.source,
	};
}

function toToolRegistry(tools: readonly BuiltTool[]): InstanceAiToolRegistry {
	const registry = createToolRegistry();
	for (const tool of tools) {
		registry.set(tool.name, tool);
	}
	return registry;
}

const BUILDER_WORKFLOW_ACTIONS = [
	'list',
	'get',
	'get-as-code',
] as const satisfies readonly WorkflowAction[];

const BUILDER_CREDENTIAL_ACTIONS = [
	'list',
	'get',
	'search-types',
	'test',
] as const satisfies readonly CredentialAction[];

// The builder owns its tool/action surface here. The generic tool factories only enforce
// the action list they are given, which keeps agent policy out of shared tools.
const BUILDER_SANDBOX_TOOL_NAMES = [
	'nodes',
	'executions',
	DATA_TABLES_TOOL_ID,
	'parse-file',
	ASK_USER_TOOL_ID,
	'research',
] as const;

const BUILDER_TOOL_MODE_TOOL_NAMES = [
	'build-workflow',
	'nodes',
	'workflows',
	DATA_TABLES_TOOL_ID,
	'parse-file',
	ASK_USER_TOOL_ID,
	'research',
] as const;

function createBuilderWorkflowsTool(context: InstanceAiContext) {
	return createWorkflowsTool(context, {
		allowedActions: BUILDER_WORKFLOW_ACTIONS,
		descriptionPrefix: 'Inspect workflows during build',
	});
}

function createBuilderCredentialsTool(context: InstanceAiContext) {
	return createCredentialsTool(context, {
		allowedActions: BUILDER_CREDENTIAL_ACTIONS,
		descriptionPrefix: 'Inspect credentials during build',
		descriptionSuffix: 'Setup is handled after workflow verification.',
	});
}

export function buildWarmBuilderFollowUp(input: {
	task: string;
	conversationContext?: string;
	workflowId: string;
	workItemId: string;
}): string {
	const parts = [
		'<builder-follow-up type="fix">',
		`Work item ID: ${input.workItemId}`,
		`Workflow ID: ${input.workflowId}`,
		'Continue from the existing sandbox files and your prior builder messages/tool calls. Apply the requested change, then submit the main workflow file.',
		'',
		DETACHED_BUILDER_REQUIREMENTS,
	];

	if (input.conversationContext) {
		parts.push('', '<conversation-context>', input.conversationContext, '</conversation-context>');
	}

	parts.push('', '<requested-change>', input.task, '</requested-change>', '</builder-follow-up>');
	return parts.join('\n');
}

/**
 * Clear the AI-builder temporary marker from the build's main workflow so the
 * run-finish reap leaves it alone. Best-effort: a failure here means the
 * main workflow gets archived at run-finish, which the user can recover
 * from the archive view.
 */
async function promoteMainWorkflow(
	context: InstanceAiContext | undefined,
	logger: Logger,
	workflowId: string | undefined,
): Promise<void> {
	if (!workflowId || !context) return;
	try {
		await context.workflowService.clearAiTemporary(workflowId);
	} catch (error) {
		logger.warn(
			`Failed to clear AI-builder temporary marker on main workflow ${workflowId}: ${
				error instanceof Error ? error.message : String(error)
			}`,
		);
	}
}

function isRecord(value: unknown): value is Record<string, unknown> {
	return typeof value === 'object' && value !== null;
}

export function recordSuccessfulWorkflowBuilds(
	tool: BuiltTool | undefined,
	onWorkflowId: (workflowId: string) => void,
): void {
	if (!tool?.handler) return;

	const original = tool.handler;
	const wrapped: NonNullable<BuiltTool['handler']> = async (input, ctx) => {
		const result = await original(input, ctx);
		if (isRecord(result) && result.success === true && typeof result.workflowId === 'string') {
			onWorkflowId(result.workflowId);
		}
		return result;
	};
	Object.assign(tool, { handler: wrapped });
}

function detectTriggerType(_attempt: SubmitWorkflowAttempt | undefined): TriggerType {
	// Every trigger type the builder can produce is testable: manual/schedule via
	// `executions(action="run")`, event-based via `verify-built-workflow` with inputData.
	// `trigger_only` is reserved for workflows the builder could not fully wire
	// (e.g. unresolved placeholders), which is detected separately via
	// `hasUnresolvedPlaceholders` in buildOutcome().
	return 'manual_or_testable';
}

export type OutcomeForVerificationReadiness = Pick<
	WorkflowBuildOutcome,
	| 'submitted'
	| 'workflowId'
	| 'triggerNodes'
	| 'mockedCredentialTypes'
	| 'mockedCredentialsByNode'
	| 'verificationPinData'
	| 'usesWorkflowPinDataForVerification'
	| 'hasUnresolvedPlaceholders'
	| 'verification'
	| 'remediation'
>;

function hasMockedCredentials(outcome: OutcomeForVerificationReadiness): boolean {
	return (
		(outcome.mockedCredentialTypes?.length ?? 0) > 0 ||
		Object.keys(outcome.mockedCredentialsByNode ?? {}).length > 0
	);
}

function hasCredentialVerificationData(outcome: OutcomeForVerificationReadiness): boolean {
	return (
		Object.keys(outcome.verificationPinData ?? {}).length > 0 ||
		outcome.usesWorkflowPinDataForVerification === true
	);
}

function hasSuccessfulStructuredVerification(outcome: OutcomeForVerificationReadiness): boolean {
	return (
		outcome.verification?.attempted === true &&
		outcome.verification.success &&
		!!outcome.verification.executionId
	);
}

export function determineVerificationReadiness(
	outcome: OutcomeForVerificationReadiness,
): WorkflowVerificationReadiness {
	if (hasSuccessfulStructuredVerification(outcome)) {
		return { status: 'already_verified' };
	}

	if (!outcome.submitted) {
		return {
			status: 'not_verifiable',
			reason: 'not-submitted',
			guidance: 'The build did not submit a workflow, so there is nothing to verify.',
		};
	}

	if (!outcome.workflowId) {
		return {
			status: 'not_verifiable',
			reason: 'missing-workflow-id',
			guidance: 'The build outcome does not include a workflow ID.',
		};
	}

	if (outcome.hasUnresolvedPlaceholders) {
		return {
			status: 'needs_setup',
			reason: 'unresolved-placeholders',
			guidance: 'Route the workflow through setup before verification.',
		};
	}

	if (hasMockedCredentials(outcome) && !hasCredentialVerificationData(outcome)) {
		return {
			status: 'needs_setup',
			reason: 'missing-mocked-credential-pin-data',
			guidance: 'Route the workflow through setup because mocked credentials cannot be verified.',
		};
	}

	if (outcome.remediation?.category === 'needs_setup') {
		return {
			status: 'needs_setup',
			reason: 'workflow-needs-setup',
			guidance: outcome.remediation.guidance,
		};
	}

	if (!outcome.triggerNodes?.some((node) => isMockableTriggerNodeType(node.nodeType))) {
		return {
			status: 'not_verifiable',
			reason: 'non-mockable-trigger',
			guidance: 'The workflow does not have a trigger the post-build verifier can exercise.',
		};
	}

	return { status: 'ready' };
}

export function determineSetupRequirement(
	outcome: OutcomeForVerificationReadiness,
): WorkflowSetupRequirement {
	if (!outcome.submitted || !outcome.workflowId) {
		return { status: 'not_required' };
	}

	if (outcome.hasUnresolvedPlaceholders) {
		return {
			status: 'required',
			reason: 'unresolved-placeholders',
			guidance: 'Route the workflow through setup so the user can fill unresolved values.',
		};
	}

	if (hasMockedCredentials(outcome)) {
		return {
			status: 'required',
			reason: 'mocked-credentials',
			guidance: 'Route the workflow through setup so the user can add real credentials.',
		};
	}

	if (outcome.remediation?.category === 'needs_setup') {
		return {
			status: 'required',
			reason: 'workflow-needs-setup',
			guidance: outcome.remediation.guidance,
		};
	}

	return { status: 'not_required' };
}

type OutcomeWithoutDeterministicRouting = Omit<
	WorkflowBuildOutcome,
	'verificationReadiness' | 'setupRequirement'
>;

function withDeterministicRouting(
	outcome: OutcomeWithoutDeterministicRouting,
): WorkflowBuildOutcome {
	return {
		...outcome,
		verificationReadiness: determineVerificationReadiness(outcome),
		setupRequirement: determineSetupRequirement(outcome),
	};
}

function buildOutcome(
	workItemId: string,
	runId: string,
	taskId: string,
	attempt: SubmitWorkflowAttempt | undefined,
	finalText: string,
	supportingWorkflowIds: string[] = [],
): WorkflowBuildOutcome {
	if (!attempt?.success) {
		return withDeterministicRouting({
			workItemId,
			runId,
			taskId,
			submitted: false,
			triggerType: 'manual_or_testable',
			needsUserInput: false,
			failureSignature: attempt?.errors?.join('; '),
			remediation: attempt?.remediation,
			summary: finalText,
		});
	}
	const placeholderRemediation = attempt.hasUnresolvedPlaceholders
		? createRemediation({
				category: 'needs_setup',
				shouldEdit: false,
				reason: 'mocked_credentials_or_placeholders',
				guidance:
					'Workflow submitted successfully, but unresolved setup values remain. Stop code edits and route to workflows(action="setup").',
			})
		: undefined;
	return withDeterministicRouting({
		workItemId,
		runId,
		taskId,
		workflowId: attempt.workflowId,
		submitted: true,
		triggerType: detectTriggerType(attempt),
		needsUserInput: Boolean(placeholderRemediation),
		blockingReason: placeholderRemediation?.guidance,
		mockedNodeNames: attempt.mockedNodeNames,
		mockedCredentialTypes: attempt.mockedCredentialTypes,
		mockedCredentialsByNode: attempt.mockedCredentialsByNode,
		triggerNodes: attempt.triggerNodes,
		verificationPinData: attempt.verificationPinData,
		usesWorkflowPinDataForVerification: attempt.usesWorkflowPinDataForVerification,
		supportingWorkflowIds: supportingWorkflowIds.length > 0 ? supportingWorkflowIds : undefined,
		hasUnresolvedPlaceholders: attempt.hasUnresolvedPlaceholders,
		remediation: placeholderRemediation ?? attempt.remediation,
		summary: finalText,
	});
}

export function mergeLatestVerificationIntoOutcome(
	outcome: WorkflowBuildOutcome,
	latestOutcome: WorkflowBuildOutcome | undefined,
): WorkflowBuildOutcome {
	if (!latestOutcome?.verification) return outcome;
	if (latestOutcome.workItemId !== outcome.workItemId) return outcome;
	if (latestOutcome.taskId !== outcome.taskId) return outcome;
	if (
		outcome.workflowId &&
		latestOutcome.workflowId &&
		latestOutcome.workflowId !== outcome.workflowId
	) {
		return outcome;
	}

	return withDeterministicRouting({
		...outcome,
		verification: latestOutcome.verification,
	});
}

export function withTerminalLoopState(
	outcome: WorkflowBuildOutcome,
	state: WorkflowLoopState | undefined,
): WorkflowBuildOutcome {
	const remediation = state?.lastRemediation;
	if (!outcome.submitted || !remediation || remediation.shouldEdit) {
		return outcome;
	}

	return withDeterministicRouting({
		...outcome,
		workflowId: outcome.workflowId ?? state.workflowId,
		needsUserInput: remediation.category === 'needs_setup',
		blockingReason: remediation.guidance,
		remediation,
	});
}

async function finalBuildOutcome(
	context: OrchestrationContext,
	workItemId: string,
	outcome: WorkflowBuildOutcome,
): Promise<WorkflowBuildOutcome> {
	const latestOutcome = await getLatestBuildOutcome(context, workItemId);
	const loopState = await context.workflowTaskService?.getWorkflowLoopState(workItemId);
	return withTerminalLoopState(
		mergeLatestVerificationIntoOutcome(outcome, latestOutcome),
		loopState,
	);
}

export async function finalizeBuildResult(
	context: OrchestrationContext,
	workItemId: string,
	result: { text: string; outcome: WorkflowBuildOutcome },
): Promise<{ text: string; outcome: WorkflowBuildOutcome }> {
	return {
		text: result.text,
		outcome: await finalBuildOutcome(context, workItemId, result.outcome),
	};
}

async function reportAndFinalizeBuildResult(
	context: OrchestrationContext,
	workItemId: string,
	result: BuildWorkflowAgentRunResult,
): Promise<BuildWorkflowAgentRunResult> {
	await context.workflowTaskService?.reportBuildOutcome(result.outcome);
	return await finalizeBuildResult(context, workItemId, result);
}

async function buildOutcomeWithLatestVerification(
	context: OrchestrationContext,
	workItemId: string,
	taskId: string,
	attempt: SubmitWorkflowAttempt | undefined,
	finalText: string,
	supportingWorkflowIds: string[] = [],
): Promise<WorkflowBuildOutcome> {
	const outcome = buildOutcome(
		workItemId,
		context.runId,
		taskId,
		attempt,
		finalText,
		supportingWorkflowIds,
	);
	return await finalBuildOutcome(context, workItemId, outcome);
}

export const DETACHED_BUILDER_REQUIREMENTS = `## Detached Task Contract

You are running as a detached background task. Do not stop after a successful submit — verify the workflow works.

### Completion criteria

Your job is done when ONE of these is true:
- the workflow is verified (ran successfully)
- you are blocked after one repair attempt per unique failure

Do NOT stop after a successful submit without verifying. Every trigger type is testable:
manual / schedule via \`executions(action="run")\`; event-based triggers (form, webhook,
chat, mcp, linear, github, slack, etc.) via \`verify-built-workflow\` with an \`inputData\`
payload. The pin-data adapter injects it as the trigger node's output.

### Submit discipline

**Every file edit MUST be followed by submit-workflow before you do anything else.**
The system tracks file hashes. If you edit the code and then call \`executions(action="run")\`, \`verify-built-workflow\`, or finish without re-submitting, your work is discarded. The sequence is always: edit → submit → then verify/run.

### Verification

- If submit-workflow returned mocked credentials, call \`verify-built-workflow\` with the workItemId and workflowId from this task.
- Otherwise pick based on trigger type:
  - **Manual / Schedule** — \`executions(action="run")\`.
  - **Form Trigger** — pass \`inputData\` as a flat field map, e.g. \`{name: "Alice", email: "a@b.c"}\`. Do NOT wrap in \`formFields\` — production Form Trigger emits fields directly on \`$json\`, and the adapter rejects wrapped payloads.
  - **Webhook** — \`verify-built-workflow\` with \`inputData\` as the body payload, e.g. \`{event: "signup", userId: "..."}\`. Adapter wraps it under \`body\`; downstream expressions use \`$json.body.<field>\`.
  - **Chat Trigger** — \`verify-built-workflow\` with \`{chatInput: "user message"}\`.
  - **Other event triggers (Linear, GitHub, Slack, MCP, etc.)** — \`verify-built-workflow\` with \`inputData\` matching the trigger's expected payload shape.
- If verify-built-workflow returns remediation with \`shouldEdit: false\`, stop editing and follow its guidance.
- If verification fails with \`shouldEdit: true\`, make one batched code repair and re-submit. Never exceed the remaining repair budget in the remediation metadata.
- If verification fails otherwise, call \`executions(action="debug")\`, fix the code, re-submit, and retry once.
- If the same failure signature repeats, stop and explain the block.

### Resource discovery

Before writing code that uses external services, **resolve real resource IDs**:
- Call \`nodes(action="explore-resources")\` for any parameter with searchListMethod (calendars, spreadsheets, channels, models, etc.)
- Do NOT use "primary", "default", or any assumed identifier — look up the actual value
- Call \`nodes(action="suggested")\` early if the workflow fits a known category (web_app, form_input, data_persistence, etc.) — the pattern hints prevent common mistakes
- Check @builderHint annotations in node type definitions for critical configuration guidance

### Publishing

Do NOT call \`workflows(action="publish")\` for the main workflow. Publishing is the user's decision after testing. Your job ends at a successful submit. The only exception is sub-workflows in the compositional pattern — those must be published so the parent workflow can reference them.
`;

function hashContent(content: string | null): string {
	return createHash('sha256')
		.update(content ?? '', 'utf8')
		.digest('hex');
}

export function createMainWorkflowSnapshot(content: string | null): MainWorkflowSnapshot {
	if (content === null) {
		return { exists: false };
	}

	return {
		exists: true,
		sourceHash: hashContent(content),
	};
}

export function shouldFinalSubmitMainWorkflow(input: {
	initial: MainWorkflowSnapshot;
	current: MainWorkflowSnapshot;
}): boolean {
	return (
		input.current.exists &&
		(!input.initial.exists || input.initial.sourceHash !== input.current.sourceHash)
	);
}

function buildNotSubmittedOutcome(
	workItemId: string,
	runId: string,
	taskId: string,
	finalText: string,
	failureSignature = WORKFLOW_NOT_SUBMITTED_FAILURE_SIGNATURE,
): WorkflowBuildOutcome {
	return withDeterministicRouting({
		workItemId,
		runId,
		taskId,
		submitted: false,
		triggerType: 'manual_or_testable',
		needsUserInput: false,
		failureSignature,
		summary: finalText,
	});
}

function deterministicSuffix(seed: string, label: string, length: number): string {
	return createHash('sha256')
		.update(label)
		.update('\0')
		.update(seed)
		.digest('hex')
		.slice(0, length);
}

function shouldUseDeterministicBuilderIds(context: OrchestrationContext): boolean {
	return process.env.E2E_TESTS === 'true' && context.tracing?.replayMode !== 'off';
}

function createDeterministicBuilderIds(input: StartBuildWorkflowAgentInput): {
	subAgentId: string;
	taskId: string;
	workItemId: string;
} {
	const seed = JSON.stringify({
		task: input.task,
		workflowId: input.workflowId ?? '',
		plannedTaskId: input.plannedTaskId ?? '',
		conversationContext: input.conversationContext ?? '',
	});

	return {
		subAgentId: `agent-builder-${deterministicSuffix(seed, 'agent', 6)}`,
		taskId: `build-${deterministicSuffix(seed, 'task', 8)}`,
		workItemId: `wi_${deterministicSuffix(seed, 'work-item', 8)}`,
	};
}

function latestSuccessfulMainSubmit(
	submitAttempts: SubmitWorkflowAttempt[],
	mainWorkflowPath: string,
): SubmitWorkflowAttempt | undefined {
	for (let i = submitAttempts.length - 1; i >= 0; i--) {
		const attempt = submitAttempts[i];
		if (attempt.filePath === mainWorkflowPath && attempt.success && attempt.workflowId) {
			return attempt;
		}
	}
	return undefined;
}

function latestMainSubmit(
	submitAttempts: SubmitWorkflowAttempt[],
	mainWorkflowPath: string,
): SubmitWorkflowAttempt | undefined {
	for (let i = submitAttempts.length - 1; i >= 0; i--) {
		const attempt = submitAttempts[i];
		if (attempt.filePath === mainWorkflowPath) {
			return attempt;
		}
	}
	return undefined;
}

export function supportingWorkflowIdsFromSubmitAttempts(
	submitAttempts: SubmitWorkflowAttempt[],
	mainWorkflowPath: string,
	mainWorkflowId: string | undefined,
	referencedWorkflowIds: string[] = [],
): string[] {
	const seen = new Set<string>();
	const referencedWorkflowIdSet = new Set(referencedWorkflowIds);
	const supportingWorkflowIds: string[] = [];

	for (const attempt of submitAttempts) {
		if (!attempt.success || !attempt.workflowId) continue;
		if (attempt.filePath === mainWorkflowPath) continue;
		if (attempt.workflowId === mainWorkflowId) continue;
		if (!referencedWorkflowIdSet.has(attempt.workflowId)) continue;
		if (seen.has(attempt.workflowId)) continue;

		seen.add(attempt.workflowId);
		supportingWorkflowIds.push(attempt.workflowId);
	}

	return supportingWorkflowIds;
}

/**
 * When the builder's stream errors mid-run, recover a successful-submit outcome
 * from the submit-attempt history so the orchestrator doesn't redo a build that
 * already produced a workflow. A later main-path submit failure is only
 * recoverable when remediation has already determined that more code edits
 * should stop; code-fixable validation/build failures must surface as failures.
 */
export function resultFromPostStreamError(input: {
	error: unknown;
	submitAttempts: SubmitWorkflowAttempt[];
	mainWorkflowPath: string;
	workItemId: string;
	runId: string;
	taskId: string;
}): { text: string; outcome: WorkflowBuildOutcome } | undefined {
	const latestAttempt = latestMainSubmit(input.submitAttempts, input.mainWorkflowPath);
	if (!latestAttempt) return undefined;

	const attempt = latestAttempt.success
		? latestAttempt
		: shouldRecoverSavedWorkflowAfterFailedSubmit(latestAttempt)
			? latestSuccessfulMainSubmit(input.submitAttempts, input.mainWorkflowPath)
			: undefined;
	if (!attempt) return undefined;

	const errorText = input.error instanceof Error ? input.error.message : String(input.error);
	const text = `Workflow ${attempt.workflowId} submitted successfully. A later step failed: ${errorText}`;
	return {
		text,
		outcome: buildOutcome(
			input.workItemId,
			input.runId,
			input.taskId,
			attempt,
			text,
			supportingWorkflowIdsFromSubmitAttempts(
				input.submitAttempts,
				input.mainWorkflowPath,
				attempt.workflowId,
				attempt.referencedWorkflowIds,
			),
		),
	};
}

export function resultFromTerminalRemediation(input: {
	remediation: RemediationMetadata;
	submitAttempts: SubmitWorkflowAttempt[];
	mainWorkflowPath: string;
	workItemId: string;
	runId: string;
	taskId: string;
}): { text: string; outcome: WorkflowBuildOutcome } {
	const latestAttempt = latestMainSubmit(input.submitAttempts, input.mainWorkflowPath);
	const attempt =
		latestAttempt &&
		!latestAttempt.success &&
		shouldRecoverSavedWorkflowAfterFailedSubmit(latestAttempt)
			? (latestSuccessfulMainSubmit(input.submitAttempts, input.mainWorkflowPath) ?? latestAttempt)
			: latestAttempt;
	const text = input.remediation.guidance;
	const outcome = buildOutcome(
		input.workItemId,
		input.runId,
		input.taskId,
		attempt,
		text,
		supportingWorkflowIdsFromSubmitAttempts(
			input.submitAttempts,
			input.mainWorkflowPath,
			attempt?.workflowId,
			attempt?.referencedWorkflowIds,
		),
	);

	return {
		text,
		outcome: withDeterministicRouting({
			...outcome,
			needsUserInput: outcome.needsUserInput || input.remediation.category === 'needs_setup',
			blockingReason: input.remediation.guidance,
			remediation: input.remediation,
		}),
	};
}

async function getWorkflowNodeSummaries(
	context: InstanceAiContext | undefined,
	workflowId: string | undefined,
): Promise<Array<{ name: string; type: string }> | undefined> {
	if (!context || !workflowId) return undefined;

	try {
		const json = await context.workflowService.getAsWorkflowJSON(workflowId);
		const summaries: Array<{ name: string; type: string }> = [];
		for (const node of json.nodes ?? []) {
			if (!node.name || !node.type) continue;
			summaries.push({ name: node.name, type: node.type });
		}
		return summaries;
	} catch {
		return undefined;
	}
}

async function getLatestBuildOutcome(
	context: OrchestrationContext,
	workItemId: string,
): Promise<WorkflowBuildOutcome | undefined> {
	try {
		return await context.workflowTaskService?.getBuildOutcome(workItemId);
	} catch {
		return undefined;
	}
}

async function compactSuccessfulBuilderMemory(input: {
	context: OrchestrationContext;
	binding: BuilderMemoryBinding;
	domainContext: InstanceAiContext | undefined;
	workflowId: string | undefined;
	workItemId: string;
	mainWorkflowPath: string;
	mainWorkflowAttempt: SubmitWorkflowAttempt;
	lastRequestedChange: string;
	finalText: string;
	shouldUseBuilderMemory: boolean;
}): Promise<void> {
	if (!input.shouldUseBuilderMemory) return;

	try {
		const [nodeSummaries, latestOutcome] = await Promise.all([
			getWorkflowNodeSummaries(input.domainContext, input.workflowId),
			getLatestBuildOutcome(input.context, input.workItemId),
		]);

		await compactBuilderMemoryThread({
			context: input.context,
			binding: input.binding,
			workflowId: input.workflowId,
			workItemId: input.workItemId,
			sourceFilePath: input.mainWorkflowPath,
			nodeSummaries,
			triggerNodes: input.mainWorkflowAttempt.triggerNodes,
			mockedNodeNames: input.mainWorkflowAttempt.mockedNodeNames,
			mockedCredentialTypes: input.mainWorkflowAttempt.mockedCredentialTypes,
			mockedCredentialsByNode: input.mainWorkflowAttempt.mockedCredentialsByNode,
			verification: latestOutcome?.verification,
			lastRequestedChange: input.lastRequestedChange,
			finalBuilderResult: input.finalText,
		});
	} catch {
		// Builder memory compaction is best-effort and must not fail the build.
	}
}

async function finalizeSuccessfulMainWorkflowSubmit(input: {
	context: OrchestrationContext;
	binding: BuilderMemoryBinding;
	domainContext: InstanceAiContext | undefined;
	workItemId: string;
	taskId: string;
	mainWorkflowPath: string;
	mainWorkflowAttempt: SubmitWorkflowAttempt;
	submitAttemptHistory: SubmitWorkflowAttempt[];
	lastRequestedChange: string;
	finalText: string;
	shouldUseBuilderMemory: boolean;
}): Promise<BuildWorkflowAgentRunResult> {
	await promoteMainWorkflow(
		input.domainContext,
		input.context.logger,
		input.mainWorkflowAttempt.workflowId,
	);
	await compactSuccessfulBuilderMemory({
		context: input.context,
		binding: input.binding,
		domainContext: input.domainContext,
		workflowId: input.mainWorkflowAttempt.workflowId,
		workItemId: input.workItemId,
		mainWorkflowPath: input.mainWorkflowPath,
		mainWorkflowAttempt: input.mainWorkflowAttempt,
		lastRequestedChange: input.lastRequestedChange,
		finalText: input.finalText,
		shouldUseBuilderMemory: input.shouldUseBuilderMemory,
	});
	const outcome = await buildOutcomeWithLatestVerification(
		input.context,
		input.workItemId,
		input.taskId,
		input.mainWorkflowAttempt,
		input.finalText,
		supportingWorkflowIdsFromSubmitAttempts(
			input.submitAttemptHistory,
			input.mainWorkflowPath,
			input.mainWorkflowAttempt.workflowId,
			input.mainWorkflowAttempt.referencedWorkflowIds,
		),
	);
	return {
		text: input.finalText,
		outcome,
	};
}

export function resultFromLaterFailedMainSubmit(input: {
	failedAttempt: SubmitWorkflowAttempt;
	submitAttempts: SubmitWorkflowAttempt[];
	mainWorkflowPath: string;
	workItemId: string;
	runId: string;
	taskId: string;
}): { text: string; outcome: WorkflowBuildOutcome } | undefined {
	if (!shouldRecoverSavedWorkflowAfterFailedSubmit(input.failedAttempt)) return undefined;

	const preservedAttempt = latestSuccessfulMainSubmit(input.submitAttempts, input.mainWorkflowPath);
	if (!preservedAttempt) return undefined;

	const errorText = input.failedAttempt.errors?.join(' ') ?? 'Unknown submit-workflow failure.';
	const text =
		`Workflow ${preservedAttempt.workflowId} was already submitted successfully. ` +
		`A later submit failed: ${errorText}`;
	return {
		text,
		outcome: buildOutcome(
			input.workItemId,
			input.runId,
			input.taskId,
			preservedAttempt,
			text,
			supportingWorkflowIdsFromSubmitAttempts(
				input.submitAttempts,
				input.mainWorkflowPath,
				preservedAttempt.workflowId,
				preservedAttempt.referencedWorkflowIds,
			),
		),
	};
}

function isFreshAttemptForHash(
	attempt: SubmitWorkflowAttempt | undefined,
	sourceHash: string,
): attempt is SubmitWorkflowAttempt {
	return attempt?.sourceHash === sourceHash;
}

export function attemptFromAutoResubmit(input: {
	latestAttempt: SubmitWorkflowAttempt | undefined;
	resubmit: SubmitWorkflowOutput;
	filePath: string;
	sourceHash: string;
}): SubmitWorkflowAttempt | undefined {
	if (isFreshAttemptForHash(input.latestAttempt, input.sourceHash)) {
		return input.latestAttempt;
	}
	if (input.resubmit.success) return undefined;
	return {
		filePath: input.filePath,
		sourceHash: input.sourceHash,
		success: false,
		errors: input.resubmit.errors,
		remediation: input.resubmit.remediation,
	};
}

export function shouldRecoverSavedWorkflowAfterFailedSubmit(
	attempt: SubmitWorkflowAttempt,
): boolean {
	return attempt.remediation?.shouldEdit === false;
}

function formatSubmitWorkflowErrors(output: SubmitWorkflowOutput, fallback: string): string {
	const errors = output.errors?.join(' ') ?? '';
	return errors.length > 0 ? errors : fallback;
}

export async function settleMissingMainWorkflowSubmit(input: {
	context: OrchestrationContext;
	workItemId: string;
	runId: string;
	taskId: string;
	workflowId: string | undefined;
	mainWorkflowPath: string;
	initialMainWorkflowSnapshot: MainWorkflowSnapshot;
	currentMainWorkflow: string | null;
	currentMainWorkflowHash: string;
	submitTool: BuiltTool | undefined;
	submitAttempts: Map<string, SubmitWorkflowAttempt>;
	submitAttemptHistory: SubmitWorkflowAttempt[];
	finalText: string;
	onSuccessfulSubmit: (attempt: SubmitWorkflowAttempt) => Promise<BuildWorkflowAgentRunResult>;
	onRecoveredSubmit: (result: BuildWorkflowAgentRunResult) => Promise<BuildWorkflowAgentRunResult>;
}): Promise<BuildWorkflowAgentRunResult> {
	const currentSnapshot = createMainWorkflowSnapshot(input.currentMainWorkflow);
	const shouldFinalSubmit = shouldFinalSubmitMainWorkflow({
		initial: input.initialMainWorkflowSnapshot,
		current: currentSnapshot,
	});
	input.context.trackTelemetry?.('Builder finished without submit', {
		thread_id: input.context.threadId,
		run_id: input.runId,
		work_item_id: input.workItemId,
		...(input.workflowId ? { workflow_id: input.workflowId } : {}),
		has_main_workflow_file: currentSnapshot.exists,
		main_workflow_changed: shouldFinalSubmit,
		final_settlement: shouldFinalSubmit
			? 'final_submit'
			: input.currentMainWorkflow === null
				? 'missing_file'
				: 'unchanged_file',
	});

	if (!shouldFinalSubmit) {
		const text =
			input.currentMainWorkflow === null
				? 'Error: workflow builder finished without creating or submitting /src/workflow.ts.'
				: 'Error: workflow builder finished without submitting /src/workflow.ts; the file was unchanged from the start of the run.';
		return await reportAndFinalizeBuildResult(input.context, input.workItemId, {
			text,
			outcome: buildNotSubmittedOutcome(input.workItemId, input.runId, input.taskId, text),
		});
	}

	if (!input.submitTool?.handler) {
		const text =
			'Error: workflow builder wrote /src/workflow.ts, but submit-workflow was unavailable for final settlement.';
		return await reportAndFinalizeBuildResult(input.context, input.workItemId, {
			text,
			outcome: buildNotSubmittedOutcome(
				input.workItemId,
				input.runId,
				input.taskId,
				text,
				WORKFLOW_FINAL_SUBMIT_FAILED_FAILURE_SIGNATURE,
			),
		});
	}

	const attemptsBeforeFinalSubmit = input.submitAttemptHistory.length;
	let finalSubmit: SubmitWorkflowOutput;
	try {
		const submitInput: Record<string, unknown> = { filePath: input.mainWorkflowPath };
		if (input.workflowId) {
			submitInput.workflowId = input.workflowId;
		}
		finalSubmit = (await input.submitTool.handler(submitInput, {})) as SubmitWorkflowOutput;
	} catch (error) {
		const message = error instanceof Error ? error.message : String(error);
		const recordedAttempt = input.submitAttempts.get(input.mainWorkflowPath);
		if (isFreshAttemptForHash(recordedAttempt, input.currentMainWorkflowHash)) {
			if (recordedAttempt.success) {
				return await input.onSuccessfulSubmit(recordedAttempt);
			}

			if (shouldRecoverSavedWorkflowAfterFailedSubmit(recordedAttempt)) {
				const recovered = resultFromLaterFailedMainSubmit({
					failedAttempt: recordedAttempt,
					submitAttempts: input.submitAttemptHistory,
					mainWorkflowPath: input.mainWorkflowPath,
					workItemId: input.workItemId,
					runId: input.runId,
					taskId: input.taskId,
				});
				if (recovered) {
					return await input.onRecoveredSubmit(recovered);
				}
			}

			const recordedErrors = recordedAttempt.errors?.join(' ') ?? message;
			const text = `Error: final submit of /src/workflow.ts failed. ${recordedErrors}`;
			return await finalizeBuildResult(input.context, input.workItemId, {
				text,
				outcome: buildOutcome(input.workItemId, input.runId, input.taskId, recordedAttempt, text),
			});
		}

		const text = `Error: final submit of /src/workflow.ts failed before recording an attempt. ${message}`;
		const result = {
			text,
			outcome: buildNotSubmittedOutcome(
				input.workItemId,
				input.runId,
				input.taskId,
				text,
				WORKFLOW_FINAL_SUBMIT_FAILED_FAILURE_SIGNATURE,
			),
		};
		if (input.submitAttemptHistory.length === attemptsBeforeFinalSubmit) {
			return await reportAndFinalizeBuildResult(input.context, input.workItemId, result);
		}
		return await finalizeBuildResult(input.context, input.workItemId, result);
	}

	const refreshedAttempt = attemptFromAutoResubmit({
		latestAttempt: input.submitAttempts.get(input.mainWorkflowPath),
		resubmit: finalSubmit,
		filePath: input.mainWorkflowPath,
		sourceHash: input.currentMainWorkflowHash,
	});
	if (finalSubmit.success && refreshedAttempt?.success) {
		return await input.onSuccessfulSubmit(refreshedAttempt);
	}

	if (
		refreshedAttempt &&
		!refreshedAttempt.success &&
		shouldRecoverSavedWorkflowAfterFailedSubmit(refreshedAttempt)
	) {
		const recovered = resultFromLaterFailedMainSubmit({
			failedAttempt: refreshedAttempt,
			submitAttempts: input.submitAttemptHistory,
			mainWorkflowPath: input.mainWorkflowPath,
			workItemId: input.workItemId,
			runId: input.runId,
			taskId: input.taskId,
		});
		if (recovered) {
			return await input.onRecoveredSubmit(recovered);
		}
	}

	const finalSubmitErrors =
		refreshedAttempt?.errors?.join(' ') ??
		formatSubmitWorkflowErrors(finalSubmit, 'Final submit did not record a main workflow attempt.');
	const text = `Error: final submit of /src/workflow.ts failed. ${finalSubmitErrors}`;
	const result = {
		text,
		outcome: refreshedAttempt
			? buildOutcome(input.workItemId, input.runId, input.taskId, refreshedAttempt, text)
			: buildNotSubmittedOutcome(
					input.workItemId,
					input.runId,
					input.taskId,
					text,
					WORKFLOW_FINAL_SUBMIT_FAILED_FAILURE_SIGNATURE,
				),
	};
	const reportedMainAttempt = input.submitAttempts.get(input.mainWorkflowPath);
	// The submit tool's onAttempt hook reports recorded main-path attempts; only
	// report here when final settlement failed before that hook captured one.
	if (!refreshedAttempt || refreshedAttempt !== reportedMainAttempt) {
		return await reportAndFinalizeBuildResult(input.context, input.workItemId, result);
	}
	return await finalizeBuildResult(input.context, input.workItemId, result);
}

export interface StartBuildWorkflowAgentInput {
	task: string;
	workflowId?: string;
	conversationContext?: string;
	workItemId?: string;
	taskId?: string;
	agentId?: string;
	plannedTaskId?: string;
}

export interface StartedWorkflowBuildTask {
	result: string;
	taskId: string;
	agentId: string;
}

export async function startBuildWorkflowAgentTask(
	context: OrchestrationContext,
	input: StartBuildWorkflowAgentInput,
): Promise<StartedWorkflowBuildTask> {
	if (!context.spawnBackgroundTask) {
		return {
			result: 'Error: background task support not available.',
			taskId: '',
			agentId: '',
		};
	}
	const spawnBackgroundTask = context.spawnBackgroundTask;

	const sharedWorkspace = context.workspace;
	const domainContext = context.domainContext;
	const useSandbox = !!sharedWorkspace && !!domainContext;

	let builderTools: InstanceAiToolRegistry;
	let prompt = BUILDER_AGENT_PROMPT;
	let credMap: CredentialMap | undefined;

	if (useSandbox) {
		credMap = await buildCredentialMap(domainContext.credentialService);
		const builderWorkflowsTool = createBuilderWorkflowsTool(domainContext);
		const builderCredentialsTool = createBuilderCredentialsTool(domainContext);

		builderTools = createToolRegistry();
		for (const name of BUILDER_SANDBOX_TOOL_NAMES) {
			const tool = context.domainTools.get(name);
			if (tool) {
				builderTools.set(name, tool);
			}
		}
		builderTools.set('workflows', builderWorkflowsTool);
		builderTools.set(CREDENTIALS_TOOL_ID, builderCredentialsTool);
		if (context.workflowTaskService && context.domainContext) {
			builderTools.set('verify-built-workflow', createVerifyBuiltWorkflowTool(context));
		}
	} else {
		builderTools = createToolRegistry();

		for (const name of BUILDER_TOOL_MODE_TOOL_NAMES) {
			const tool = context.domainTools.get(name);
			if (tool) {
				builderTools.set(name, tool);
			}
		}
		if (domainContext) {
			builderTools.set('workflows', createBuilderWorkflowsTool(domainContext));
			builderTools.set(CREDENTIALS_TOOL_ID, createBuilderCredentialsTool(domainContext));
		}

		if (!builderTools.has('build-workflow')) {
			return { result: 'Error: build-workflow tool not available.', taskId: '', agentId: '' };
		}
	}

	const deterministicIds = shouldUseDeterministicBuilderIds(context)
		? createDeterministicBuilderIds(input)
		: undefined;
	const subAgentId = input.agentId ?? deterministicIds?.subAgentId ?? `agent-builder-${nanoid(6)}`;
	const taskId = input.taskId ?? deterministicIds?.taskId ?? `build-${nanoid(8)}`;
	const baseWorkItemId =
		input.workItemId ??
		deterministicIds?.workItemId ??
		(input.workflowId ? `${context.runId}:default` : `wi_${nanoid(8)}`);

	const { workflowId } = input;
	const workItemId = baseWorkItemId;
	const relativeMainWorkflowPath = builderWorkflowWorkspaceLayout(
		'',
		workItemId,
	).relativeMainWorkflowPath;
	const builderThreadId = randomUUID();
	const builderResourceId = createSubAgentResourceId(context.threadId, 'workflow-builder');
	const builderMemoryBinding: BuilderMemoryBinding = {
		resource: builderResourceId,
		thread: builderThreadId,
	};

	// Build additional context based on sandbox mode and existing workflow
	let additionalContext = '';
	if (useSandbox && workflowId) {
		additionalContext = `[CONTEXT: Modifying existing workflow ${workflowId}. The current code is pre-loaded in ${relativeMainWorkflowPath} — read it first, then edit. Use workflowId "${workflowId}" when calling submit-workflow.]\n\n[WORK ITEM ID: ${workItemId}]`;
	} else if (useSandbox) {
		additionalContext = `[WORK ITEM ID: ${workItemId}]`;
	} else if (workflowId) {
		additionalContext = `[CONTEXT: Modifying existing workflow ${workflowId}. Use workflowId "${workflowId}" when calling build-workflow.]`;
	}

	const runningTaskSummaries = context.getRunningTaskSummaries?.();
	const briefing = await buildSubAgentBriefing({
		task: input.task,
		conversationContext: input.conversationContext,
		additionalContext: additionalContext || undefined,
		requirements: useSandbox ? DETACHED_BUILDER_REQUIREMENTS : undefined,
		iteration: context.iterationLog
			? {
					log: context.iterationLog,
					threadId: context.threadId,
					taskKey: `build:${workflowId ?? 'new'}`,
				}
			: undefined,
		runningTasks: runningTaskSummaries,
	});
	const detachedTraceFactory = createDetachedSubAgentTraceFactory(context, {
		agentId: subAgentId,
		role: 'workflow-builder',
		kind: 'builder',
		taskId,
		plannedTaskId: input.plannedTaskId,
		workItemId,
		inputs: {
			task: input.task,
			workflowId: input.workflowId,
			conversationContext: input.conversationContext,
		},
	});
	const createTraceContext = async () => await detachedTraceFactory();

	const spawnOutcome = spawnBackgroundTask({
		taskId,
		threadId: context.threadId,
		agentId: subAgentId,
		role: 'workflow-builder',
		createTraceContext,
		plannedTaskId: input.plannedTaskId,
		workItemId,
		dedupeKey: {
			role: 'workflow-builder',
			plannedTaskId: input.plannedTaskId,
			workflowId: input.workflowId,
		},
		// When the orchestrator spawns a builder inside a checkpoint follow-up
		// (e.g. to patch a runtime bug the verify exposed), tag the task so the
		// safety net doesn't pre-emptively fail the checkpoint and the
		// settlement path can re-enter the checkpoint context instead of a
		// bare background-task-completed shell.
		parentCheckpointId:
			context.isCheckpointFollowUp === true ? context.checkpointTaskId : undefined,
		run: async (
			signal,
			drainCorrections,
			waitForCorrection,
			{ traceContext },
		): Promise<BackgroundTaskResult> =>
			await withTraceContextActor(traceContext, async () => {
				const submitAttempts = new Map<string, SubmitWorkflowAttempt>();
				// Append-only history so a later failed submit for the main path
				// cannot mask an earlier successful submit during post-error recovery.
				const submitAttemptHistory: SubmitWorkflowAttempt[] = [];
				if (useSandbox && sharedWorkspace && domainContext) {
					let workspace = sharedWorkspace;
					const root = await getWorkspaceRoot(workspace);
					const materializedRuntimeSkills = await materializeBuilderRuntimeSkills(
						context,
						workspace,
						root,
					);
					workspace = materializedRuntimeSkills.workspace;
					const runtimeSkills = materializedRuntimeSkills.source;
					const builderLayout = builderWorkflowWorkspaceLayout(root, workItemId);
					let telemetrySession: TemplateTelemetrySession | undefined;
					let unsubscribeTelemetry: (() => void) | undefined;

					try {
						telemetrySession = createTemplateTelemetrySession({
							context,
							threadId: context.threadId,
							runId: context.runId,
							workItemId,
							userRequestExcerpt: input.task,
							templatesVersion: domainContext.templatesService?.getVersion() ?? null,
						});
						attachTemplateTelemetrySession(workspace, telemetrySession);
						const templateToolObserver = createTypedToolObserver(telemetrySession);
						unsubscribeTelemetry = context.eventBus.subscribe(context.threadId, (stored) => {
							if (stored.event.agentId !== subAgentId) return;
							templateToolObserver(stored.event);
						});

						prompt = createSandboxBuilderAgentPrompt(root, {
							mainWorkflowPath: builderLayout.mainWorkflowPath,
							sourceDir: builderLayout.sourceDir,
							chunksDir: builderLayout.chunksDir,
							tsconfigPath: builderLayout.tsconfigPath,
						});
						await writeBuilderWorkspaceFile(
							workspace,
							builderLayout.tsconfigPath,
							renderBuilderTaskTsconfig(),
						);

						if (workflowId) {
							try {
								const json = await domainContext.workflowService.getAsWorkflowJSON(workflowId);
								const rawCode = generateWorkflowCode(json);
								const code = `${SDK_IMPORT_STATEMENT}\n\n${rawCode}`;
								await writeBuilderWorkspaceFile(workspace, builderLayout.mainWorkflowPath, code);
							} catch {
								// Non-fatal — agent can still build from scratch
							}
						} else {
							await writeBuilderWorkspaceFile(
								workspace,
								builderLayout.mainWorkflowPath,
								`${SDK_IMPORT_STATEMENT}\n\n`,
							);
						}

						const mainWorkflowPath = builderLayout.mainWorkflowPath;
						const initialMainWorkflowSnapshot = createMainWorkflowSnapshot(
							await readFileViaSandbox(workspace, mainWorkflowPath),
						);
						builderTools.set(
							'submit-workflow',
							createIdentityEnforcedSubmitWorkflowTool({
								context: domainContext,
								workspace,
								credentialMap: credMap,
								root,
								defaultFilePath: mainWorkflowPath,
								currentRunId: context.runId,
								getWorkflowLoopState: async () =>
									await context.workflowTaskService?.getWorkflowLoopState(workItemId),
								onGuardFired: (event) => {
									context.trackTelemetry?.('Builder remediation guard fired', {
										thread_id: context.threadId,
										run_id: context.runId,
										work_item_id: workItemId,
										workflow_id: event.workflowId,
										category: event.category,
										attempt_count: event.attemptCount,
										reason: event.reason,
									});
								},
								onAttempt: async (attempt) => {
									submitAttempts.set(attempt.filePath, attempt);
									submitAttemptHistory.push(attempt);
									if (attempt.filePath !== mainWorkflowPath) {
										return;
									}
									if (!context.workflowTaskService) {
										return;
									}

									await context.workflowTaskService.reportBuildOutcome(
										buildOutcome(
											workItemId,
											context.runId,
											taskId,
											attempt,
											attempt.success
												? 'Workflow submitted and ready for verification.'
												: (attempt.errors?.join(' ') ?? 'Workflow submission failed.'),
										),
									);
								},
							}),
						);

						const tracedBuilderTools = traceSubAgentTools(
							context,
							builderTools,
							'workflow-builder',
						);
						const runtimeWorkspaceTools = toToolRegistry(workspace.getTools());
						const builderMemory = getBuilderSessionMemory(context, true);
						const shouldUseBuilderMemory = Boolean(builderMemory);

						const subAgent = new Agent('Workflow Builder Agent')
							.model(context.modelId)
							.instructions(prompt, {
								providerOptions: {
									anthropic: { cacheControl: { type: 'ephemeral' } },
								},
							})
							.tool(toolRegistryValues(tracedBuilderTools))
							.checkpoint(context.checkpointStore ?? 'memory');
						attachRuntimeWorkspaceCapabilities(subAgent, { workspace, runtimeSkills });
						if (builderMemory) {
							subAgent.memory(builderMemory);
						}
						const telemetry = traceContext?.getTelemetry?.({
							agentRole: 'workflow-builder',
							functionId: 'instance-ai.subagent.workflow-builder',
							executionMode: 'background_subagent',
							metadata: { agent_id: subAgentId, task_id: taskId },
						});
						if (telemetry) {
							subAgent.telemetry(telemetry);
						}
						mergeTraceRunInputs(
							traceContext?.actorRun,
							buildAgentTraceInputs({
								systemPrompt: prompt,
								tools: tracedBuilderTools,
								runtimeTools: runtimeWorkspaceTools,
								runtimeSkills: runtimeSkills?.registry,
								modelId: context.modelId,
							}),
						);

						let finalText: string;
						try {
							const persistence = await createSubAgentPersistence(context, {
								agentKind: 'workflow-builder',
								threadId: builderThreadId,
								resourceId: builderResourceId,
							});
							const resumeOptions: Record<string, unknown> = {
								providerOptions: {
									anthropic: { cacheControl: { type: 'ephemeral' } },
								},
							};
							const stream = await subAgent.stream(briefing, {
								maxIterations: MAX_STEPS.BUILDER,
								abortSignal: signal,
								persistence,
								providerOptions: {
									anthropic: { cacheControl: { type: 'ephemeral' } },
								},
							});

							const hitlResult = await consumeStreamWithHitl({
								agent: subAgent,
								stream,
								runId: context.runId,
								agentId: subAgentId,
								eventBus: context.eventBus,
								logger: context.logger,
								threadId: context.threadId,
								abortSignal: signal,
								waitForConfirmation: context.waitForConfirmation,
								drainCorrections,
								waitForCorrection,
								maxIterations: MAX_STEPS.BUILDER,
								resumeOptions,
								persistence,
							});

							finalText = await requireCompletedHitlText(hitlResult, 'Workflow builder sub-agent');
						} catch (error) {
							const recovered = resultFromPostStreamError({
								error,
								submitAttempts: submitAttemptHistory,
								mainWorkflowPath,
								workItemId,
								runId: context.runId,
								taskId,
							});
							if (recovered) {
								await promoteMainWorkflow(
									domainContext,
									context.logger,
									recovered.outcome.workflowId,
								);
								return await finalizeBuildResult(context, workItemId, recovered);
							}
							throw error;
						}

						const mainWorkflowAttempt = submitAttempts.get(mainWorkflowPath);
						const currentMainWorkflow = await readFileViaSandbox(workspace, mainWorkflowPath);
						const currentMainWorkflowHash = hashContent(currentMainWorkflow);

						if (!mainWorkflowAttempt) {
							return await settleMissingMainWorkflowSubmit({
								context,
								workItemId,
								runId: context.runId,
								taskId,
								workflowId,
								mainWorkflowPath,
								initialMainWorkflowSnapshot,
								currentMainWorkflow,
								currentMainWorkflowHash,
								submitTool: tracedBuilderTools.get('submit-workflow'),
								submitAttempts,
								submitAttemptHistory,
								finalText,
								onSuccessfulSubmit: async (attempt) =>
									await finalizeSuccessfulMainWorkflowSubmit({
										context,
										binding: builderMemoryBinding,
										domainContext,
										workItemId,
										taskId,
										mainWorkflowPath,
										mainWorkflowAttempt: attempt,
										submitAttemptHistory,
										lastRequestedChange: input.task,
										finalText,
										shouldUseBuilderMemory,
									}),
								onRecoveredSubmit: async (recovered) => {
									await promoteMainWorkflow(
										domainContext,
										context.logger,
										recovered.outcome.workflowId,
									);
									return await finalizeBuildResult(context, workItemId, recovered);
								},
							});
						}

						if (!mainWorkflowAttempt.success) {
							const recovered = resultFromLaterFailedMainSubmit({
								failedAttempt: mainWorkflowAttempt,
								submitAttempts: submitAttemptHistory,
								mainWorkflowPath,
								workItemId,
								runId: context.runId,
								taskId,
							});
							if (recovered) {
								await promoteMainWorkflow(
									domainContext,
									context.logger,
									recovered.outcome.workflowId,
								);
								return await finalizeBuildResult(context, workItemId, recovered);
							}

							const errorText =
								mainWorkflowAttempt.errors?.join(' ') ?? 'Unknown submit-workflow failure.';
							const text = `Error: workflow builder stopped after a failed submit-workflow for ${mainWorkflowPath}. ${errorText}`;
							return {
								text,
								outcome: buildOutcome(workItemId, context.runId, taskId, mainWorkflowAttempt, text),
							};
						}

						if (mainWorkflowAttempt.sourceHash !== currentMainWorkflowHash) {
							// Builder edited the file after its last submit — auto-re-submit
							// instead of discarding the agent's work.
							const submitTool = tracedBuilderTools.get('submit-workflow');
							if (submitTool?.handler) {
								const resubmit = (await submitTool.handler(
									{
										filePath: mainWorkflowPath,
										workflowId: mainWorkflowAttempt.workflowId,
									},
									{},
								)) as SubmitWorkflowOutput;

								const refreshedAttempt = attemptFromAutoResubmit({
									latestAttempt: submitAttempts.get(mainWorkflowPath),
									resubmit,
									filePath: mainWorkflowPath,
									sourceHash: currentMainWorkflowHash,
								});
								if (resubmit.success && refreshedAttempt?.success) {
									await promoteMainWorkflow(
										domainContext,
										context.logger,
										refreshedAttempt.workflowId,
									);
									await compactSuccessfulBuilderMemory({
										context,
										binding: builderMemoryBinding,
										domainContext,
										workflowId: refreshedAttempt.workflowId,
										workItemId,
										mainWorkflowPath,
										mainWorkflowAttempt: refreshedAttempt,
										lastRequestedChange: input.task,
										finalText,
										shouldUseBuilderMemory,
									});
									const outcome = await buildOutcomeWithLatestVerification(
										context,
										workItemId,
										taskId,
										refreshedAttempt,
										finalText,
									);
									return {
										text: finalText,
										outcome,
									};
								}

								const resubmitErrors =
									refreshedAttempt?.errors?.join(' ') ??
									formatSubmitWorkflowErrors(resubmit, 'Auto-re-submit failed.');
								if (
									refreshedAttempt &&
									!refreshedAttempt.success &&
									shouldRecoverSavedWorkflowAfterFailedSubmit(refreshedAttempt)
								) {
									const recovered = resultFromLaterFailedMainSubmit({
										failedAttempt: refreshedAttempt,
										submitAttempts: submitAttemptHistory,
										mainWorkflowPath,
										workItemId,
										runId: context.runId,
										taskId,
									});
									if (recovered) {
										await promoteMainWorkflow(
											domainContext,
											context.logger,
											recovered.outcome.workflowId,
										);
										return await finalizeBuildResult(context, workItemId, recovered);
									}
								}
								const text = `Error: auto-re-submit of edited ${mainWorkflowPath} failed. ${resubmitErrors}`;
								return {
									text,
									outcome: buildOutcome(
										workItemId,
										context.runId,
										taskId,
										refreshedAttempt ?? undefined,
										text,
									),
								};
							}
						}

						await promoteMainWorkflow(
							domainContext,
							context.logger,
							mainWorkflowAttempt.workflowId,
						);
						await compactSuccessfulBuilderMemory({
							context,
							binding: builderMemoryBinding,
							domainContext,
							workflowId: mainWorkflowAttempt.workflowId,
							workItemId,
							mainWorkflowPath,
							mainWorkflowAttempt,
							lastRequestedChange: input.task,
							finalText,
							shouldUseBuilderMemory,
						});
						const outcome = await buildOutcomeWithLatestVerification(
							context,
							workItemId,
							taskId,
							mainWorkflowAttempt,
							finalText,
						);
						return {
							text: finalText,
							outcome,
						};
					} finally {
						unsubscribeTelemetry?.();
						if (telemetrySession) {
							try {
								telemetrySession.flush();
								detachTemplateTelemetrySession(workspace);
							} catch (error) {
								context.logger.warn('build-workflow-agent: failed to flush template telemetry', {
									error: error instanceof Error ? error.message : String(error),
								});
							}
						}
					}
				}

				let fallbackMainWorkflowId: string | undefined;
				recordSuccessfulWorkflowBuilds(builderTools.get('build-workflow'), (workflowId) => {
					fallbackMainWorkflowId = workflowId;
				});

				const tracedBuilderTools = traceSubAgentTools(context, builderTools, 'workflow-builder');
				const runtimeSkills = context.runtimeSkills;

				const subAgent = new Agent('Workflow Builder Agent')
					.model(context.modelId)
					.instructions(prompt, {
						providerOptions: {
							anthropic: { cacheControl: { type: 'ephemeral' } },
						},
					})
					.tool(toolRegistryValues(tracedBuilderTools))
					.checkpoint(context.checkpointStore ?? 'memory');
				attachRuntimeWorkspaceCapabilities(subAgent, { runtimeSkills });
				const telemetry = traceContext?.getTelemetry?.({
					agentRole: 'workflow-builder',
					functionId: 'instance-ai.subagent.workflow-builder',
					executionMode: 'background_subagent',
					metadata: { agent_id: subAgentId, task_id: taskId },
				});
				if (telemetry) {
					subAgent.telemetry(telemetry);
				}
				mergeTraceRunInputs(
					traceContext?.actorRun,
					buildAgentTraceInputs({
						systemPrompt: prompt,
						tools: tracedBuilderTools,
						runtimeSkills: runtimeSkills?.registry,
						modelId: context.modelId,
					}),
				);

				const resumeOptions: Record<string, unknown> = {
					providerOptions: {
						anthropic: { cacheControl: { type: 'ephemeral' } },
					},
				};
				const persistence = await createSubAgentPersistence(context, {
					agentKind: 'workflow-builder',
					threadId: builderThreadId,
					resourceId: builderResourceId,
				});
				const stream = await subAgent.stream(briefing, {
					maxIterations: MAX_STEPS.BUILDER,
					abortSignal: signal,
					persistence,
					providerOptions: {
						anthropic: { cacheControl: { type: 'ephemeral' } },
					},
				});

				const hitlResult = await consumeStreamWithHitl({
					agent: subAgent,
					stream,
					runId: context.runId,
					agentId: subAgentId,
					eventBus: context.eventBus,
					logger: context.logger,
					threadId: context.threadId,
					abortSignal: signal,
					waitForConfirmation: context.waitForConfirmation,
					drainCorrections,
					waitForCorrection,
					maxIterations: MAX_STEPS.BUILDER,
					resumeOptions,
					persistence,
				});

				const toolFinalText = await requireCompletedHitlText(
					hitlResult,
					'Workflow builder sub-agent',
				);
				await promoteMainWorkflow(domainContext, context.logger, fallbackMainWorkflowId);
				return { text: toolFinalText };
			}),
	});

	if (spawnOutcome.status === 'duplicate') {
		return {
			result: `Workflow build already in progress (task: ${spawnOutcome.existing.taskId}). Acknowledge and wait for the planned-task-follow-up — do not dispatch again.`,
			taskId: spawnOutcome.existing.taskId,
			agentId: spawnOutcome.existing.agentId,
		};
	}
	if (spawnOutcome.status === 'limit-reached') {
		return {
			result:
				'Could not start build: concurrent background-task limit reached. Wait for an existing task to finish and try again.',
			taskId: '',
			agentId: '',
		};
	}

	// Spawn confirmed — publish the UI event now so duplicate/limit-reached
	// rejections above don't leave a phantom builder card on the chat surface.
	context.eventBus.publish(context.threadId, {
		type: 'agent-spawned',
		runId: context.runId,
		agentId: subAgentId,
		payload: {
			parentId: context.orchestratorAgentId,
			role: 'workflow-builder',
			tools: toolRegistryKeys(builderTools),
			taskId,
			kind: 'builder',
			title: 'Building workflow',
			subtitle: truncateLabel(input.task),
			goal: input.task,
			targetResource: input.workflowId
				? { type: 'workflow' as const, id: input.workflowId }
				: { type: 'workflow' as const },
		},
	});

	return {
		result: `Workflow build started (task: ${taskId}). Reply with one short sentence — e.g. name what's being built. Do NOT summarize the plan or list details.`,
		taskId,
		agentId: subAgentId,
	};
}

export const buildWorkflowAgentInputSchema = z.object({
	task: z
		.string()
		.describe(
			'What to build and any context: user requirements, available credential names/types.',
		),
	workflowId: z
		.string()
		.optional()
		.describe(
			'Existing workflow ID to modify. When provided, the agent starts with the current workflow code pre-loaded.',
		),
	conversationContext: z
		.string()
		.optional()
		.describe(
			'Brief summary of the conversation so far — what was discussed, decisions made, and information gathered (e.g., which credentials are available). The builder uses this to avoid repeating information the user already knows.',
		),
	workItemId: z
		.string()
		.optional()
		.describe(
			'Workflow-loop work item ID. Required for repair builds so remediation budgets continue on the same work item.',
		),
	bypassPlan: z
		.boolean()
		.optional()
		.describe(
			'Set to true for any edit to an existing workflow — adding/removing/rewiring a node, changing an expression, swapping a credential, changing a schedule, fixing a Code node. Requires an existing `workflowId` and a one-sentence `reason`. The orchestrator verifies the result afterwards via `verify-built-workflow` when the trigger is mockable. ' +
				'A runtime guard rejects direct calls without `bypassPlan: true` outside replan/checkpoint follow-ups: new workflow builds, multi-workflow work, and data-table schema changes must go through `plan` so the build gets its orchestrator-run checkpoint.',
		),
	reason: z
		.string()
		.optional()
		.describe(
			'One sentence explaining why the planner is being bypassed (e.g. "swap Slack channel on workflow X", "fix Code node shape issue"). Required when bypassPlan is true.',
		),
});

const buildWorkflowAgentSuspendSchema = z.object({
	requestId: z.string(),
	message: z.string(),
	severity: z.literal('warning'),
});

const buildWorkflowAgentResumeSchema = z.object({
	approved: z.boolean(),
});

/**
 * Replan / checkpoint follow-ups have already paid the planner's discovery cost
 * and carry the checkpoint task graph from the original plan — direct builder
 * calls in those contexts are legitimate (e.g. retry the one failing task).
 */
function isPostPlanFollowUp(context: OrchestrationContext): boolean {
	return context.isReplanFollowUp === true || context.isCheckpointFollowUp === true;
}

function isBuildViaPlanGuardEnabled(): boolean {
	const raw = process.env.N8N_INSTANCE_AI_ENFORCE_BUILD_VIA_PLAN;
	if (raw === undefined) return true;
	return raw.toLowerCase() !== 'false' && raw !== '0';
}

const PLAN_GUARD_REJECTION_LIMIT = 3;

async function resolveWorkflowNameForEditConfirmation(
	context: OrchestrationContext,
	workflowId: string,
): Promise<string> {
	try {
		const workflow = await context.domainContext?.workflowService.get(workflowId);
		const workflowName = workflow?.name?.trim();
		return workflowName && workflowName.length > 0 ? workflowName : workflowId;
	} catch {
		return workflowId;
	}
}

export function createBuildWorkflowAgentTool(context: OrchestrationContext) {
	let planGuardRejectionCount = 0;

	const rejectPlanGuardCall = (result: string) => {
		planGuardRejectionCount++;
		if (planGuardRejectionCount >= PLAN_GUARD_REJECTION_LIMIT) {
			context.logger.warn(
				'build-workflow-with-agent plan-guard rejection limit reached — aborting run',
				{
					threadId: context.threadId,
					rejectionCount: planGuardRejectionCount,
				},
			);
			throw new UserError(
				'Stopped: the agent looped on `build-workflow-with-agent` rejections without correcting them. Try again or rephrase the request.',
			);
		}

		return { result, taskId: '' };
	};

	return new Tool('build-workflow-with-agent')
		.description(
			'Build or modify an n8n workflow using a specialized builder agent. ' +
				'The agent handles node discovery, schema lookups, code generation, and validation internally. ' +
				'For edits to an existing workflow, call directly with `bypassPlan: true`, the existing `workflowId`, and a one-sentence `reason` — the orchestrator runs a lightweight verify afterwards. ' +
				'For new workflows, multi-workflow builds, or data-table schema changes, go through `plan` — ' +
				'a runtime guard rejects direct calls without `bypassPlan: true` outside replan/checkpoint follow-ups, because those paths need the orchestrator-run checkpoint for end-to-end verification.',
		)
		.input(buildWorkflowAgentInputSchema)
		.output(
			z.object({
				result: z.string(),
				taskId: z.string(),
			}),
		)
		.suspend(buildWorkflowAgentSuspendSchema)
		.resume(buildWorkflowAgentResumeSchema)
		.handler(async (input, ctx) => {
			const isPostPlanFollowUpRun = isPostPlanFollowUp(context);
			if (isBuildViaPlanGuardEnabled() && !isPostPlanFollowUpRun) {
				if (!input.bypassPlan) {
					context.logger.warn(
						'build-workflow-with-agent called outside plan/replan context — rejecting',
						{
							threadId: context.threadId,
							hasWorkflowId: Boolean(input.workflowId),
						},
					);
					return rejectPlanGuardCall(
						'STOP. Direct builder calls require `bypassPlan: true` + an existing ' +
							'`workflowId` + a one-sentence `reason`. Use that combination for any edit to ' +
							'an existing workflow. For new workflows, multi-workflow builds, or data-table ' +
							'schema changes, call `plan` with a `build-workflow` task instead — the planner ' +
							'discovers credentials, data tables, and best practices, and schedules an ' +
							'orchestrator-run verification checkpoint.',
					);
				}
				if (!input.workflowId) {
					return rejectPlanGuardCall(
						'STOP. `bypassPlan: true` is for edits to an EXISTING workflow and requires a ' +
							'`workflowId`. New workflow builds must go through `plan` so an orchestrator-run ' +
							'verification checkpoint is scheduled. Call `plan` with a `build-workflow` task ' +
							'instead.',
					);
				}
				if (!input.reason || input.reason.trim().length === 0) {
					return rejectPlanGuardCall(
						'STOP. `bypassPlan: true` requires a one-sentence `reason` describing the edit ' +
							'(e.g. "swap Slack channel", "fix Code node shape issue").',
					);
				}
				context.logger.warn('build-workflow-with-agent bypassing plan with bypassPlan=true', {
					threadId: context.threadId,
					workflowId: input.workflowId,
					reason: input.reason,
				});
			}
			planGuardRejectionCount = 0;

			if (input.workflowId && !isPostPlanFollowUpRun && context.domainContext) {
				const updateWorkflowPermission =
					context.domainContext.permissions?.updateWorkflow ?? 'require_approval';
				if (updateWorkflowPermission === 'blocked') {
					return { result: 'Action blocked by admin', taskId: '' };
				}

				const isOwnInFlightWorkflow =
					context.domainContext.aiCreatedWorkflowIds?.has(input.workflowId) ?? false;

				if (!isOwnInFlightWorkflow) {
					const resumeData = ctx.resumeData;
					const needsApproval = updateWorkflowPermission !== 'always_allow';

					if (needsApproval && (resumeData === undefined || resumeData === null)) {
						const workflowName = await resolveWorkflowNameForEditConfirmation(
							context,
							input.workflowId,
						);
						return await ctx.suspend({
							requestId: nanoid(),
							message: `Edit ${workflowName} (ID: ${input.workflowId})`,
							severity: 'warning',
						});
					}

					if (resumeData !== undefined && resumeData !== null && !resumeData.approved) {
						return { result: 'User declined the workflow edit.', taskId: '' };
					}
				}
			}

			const result = await startBuildWorkflowAgentTask(context, input);
			return { result: result.result, taskId: result.taskId };
		})
		.build();
}
