/**
 * Verify Built Workflow Tool
 *
 * Runs a built workflow using sidecar verification pin data from the build outcome.
 * The verification pin data is never persisted to the workflow; it only exists
 * for this execution.
 */

import { Tool } from '@n8n/agents';
import { z } from 'zod';

import type { OrchestrationContext } from '../../types';
import { analyzeVerificationResult, buildNodePreviews } from './verification/analyze-result';
import {
	handleMissingSimulationPlan,
	persistVerificationOutcome,
} from './verification/finalize-result';
import { prepareVerificationRun } from './verification/prepare-run';
import { reconcileStaleCredentialPlan } from './verification/reconcile-plan';
import { resolveVerificationTarget } from './verification/resolve-target';
import { runScriptedGateVerification } from './verification/scripted-gate-run';
import { executionNodeErrorSchema } from '../../workflow-loop/workflow-loop-state';
import { collectChatModelRecoveryContext } from '../workflows/chat-model-validation';

const DEFAULT_NODE_PREVIEW_CHARS = 600;

export const verifyBuiltWorkflowInputSchema = z.object({
	workItemId: z
		.string()
		.optional()
		.describe(
			'The work item ID from the build (wi_XXXXXXXX). Optional for follow-up verification; when omitted, the latest build outcome for workflowId in this thread is used.',
		),
	workflowId: z.string().describe('The workflow ID to verify'),
	inputData: z
		.record(z.unknown())
		.optional()
		.describe(
			"Input data for the workflow trigger. Shape MUST match the trigger's real-world output: " +
				'Form Trigger -> flat field map like {name: "Alice", email: "a@b.c"} (do NOT wrap in formFields); ' +
				'Webhook -> the body payload like {event: "signup", userId: "..."} (adapter wraps it under body); ' +
				'Chat Trigger -> {chatInput: "user message"}; ' +
				'Schedule Trigger -> omit inputData. ' +
				"If you wrap a form payload in {formFields: {...}} the adapter will reject the call; the builder's " +
				'downstream expressions reference $json.<field>, matching the flat production shape.',
		),
	triggerNodeName: z
		.string()
		.min(1)
		.optional()
		.describe(
			'Name of the trigger node to start verification from. REQUIRED when the workflow has ' +
				'more than one trigger: without it a single trigger is auto-detected and the other ' +
				"triggers' branches are never verified. To cover every branch, call verify once per " +
				"trigger. Trigger names come from build-workflow's `triggerNodes` or " +
				'workflows(action="get-as-code"). Never disable, delete, reorder, or re-save a workflow — ' +
				'and never build a throwaway copy — to reach a branch; use this instead.',
		),
	timeout: z
		.number()
		.int()
		.min(1000)
		.max(600_000)
		.optional()
		.describe('Max wait time in milliseconds (default 300000)'),
	includeData: z
		.boolean()
		.optional()
		.describe('Set true only when you need the full execution data payload. Default false.'),
	maxDataChars: z
		.number()
		.int()
		.min(0)
		.max(20_000)
		.optional()
		.describe('Max characters per node preview in the compact response (default 600).'),
	fixtureOverrides: z
		.record(z.array(z.record(z.unknown())))
		.optional()
		.describe(
			'Optional per-run output fixtures keyed by node name. Only nodes already classified as simulated in the build outcome may be overridden. Use this for alternate deterministic scenarios, not raw trigger input. ' +
				'An empty array is rejected unless the node is also listed in `allowZeroItemFixtures`.',
		),
	allowZeroItemFixtures: z
		.array(z.string())
		.optional()
		.describe(
			'Node names whose `fixtureOverrides` entry may be an empty array. Zero items stop every node below, so the run reports success while verifying nothing. ' +
				'List a node here only when the empty branch is what you are verifying, and say so in your report.',
		),
});

const remediationOutputSchema = z
	.object({
		category: z.enum(['code_fixable', 'needs_setup', 'blocked']),
		shouldEdit: z.boolean(),
		guidance: z.string(),
		reason: z.string().optional(),
		remainingSubmitFixes: z.number().int().min(0).optional(),
		attemptCount: z.number().int().min(0).optional(),
	})
	.optional();

const verifyBuiltWorkflowOutputSchema = z.object({
	resolvedWorkItemId: z.string().optional(),
	executionId: z.string().optional(),
	success: z.boolean(),
	status: z.enum(['running', 'success', 'error', 'waiting', 'unknown']).optional(),
	nodesExecuted: z.array(z.string()).optional(),
	nodePreviews: z
		.array(
			z.object({
				nodeName: z.string(),
				itemCount: z.number().optional(),
				preview: z.string(),
				truncated: z.boolean(),
				chars: z.number(),
				simulated: z.boolean().optional(),
			}),
		)
		.optional(),
	simulatedNodes: z.array(z.object({ nodeName: z.string(), reason: z.string() })).optional(),
	simulationNote: z.string().optional(),
	lastNodeExecuted: z.string().optional(),
	nodeErrors: z.array(executionNodeErrorSchema).optional(),
	nodesNotReached: z.array(z.string()).optional(),
	coverageNote: z.string().optional(),
	data: z.record(z.unknown()).optional(),
	error: z.string().optional(),
	remediation: remediationOutputSchema,
	guidance: z.string().optional(),
});

type VerifyInput = z.infer<typeof verifyBuiltWorkflowInputSchema>;

export function createVerifyBuiltWorkflowTool(context: OrchestrationContext) {
	return new Tool('verify-built-workflow')
		.description(
			'Standard post-build verifier: runs a built workflow with sidecar verification context from the build outcome ' +
				'(pin data, mocked credentials, trigger-shaped inputData; all trigger types supported). ' +
				'Use `executions(action="run")` only for ad hoc runs outside build verification. ' +
				'CRITICAL: `inputData` shape depends on the trigger type (see the field description) — a wrong shape produces ' +
				'null downstream values that look like an expression bug; re-run verify with the correct shape instead of patching the workflow.',
		)
		.input(verifyBuiltWorkflowInputSchema)
		.output(verifyBuiltWorkflowOutputSchema)
		.handler(async (input: VerifyInput) => {
			const targetResult = await resolveVerificationTarget(input, context);
			if (targetResult.kind === 'blocked') return targetResult.result;
			const { target } = targetResult;
			const { input: resolvedInput, workflowId, workflowTaskService } = target;

			// Credentials assigned after the build never rebuild the plan, so
			// refresh stale mocked-credential verdicts before pinning.
			const buildOutcome = await reconcileStaleCredentialPlan({
				buildOutcome: target.buildOutcome,
				workflowId,
				domainContext: target.domainContext,
				workflowTaskService,
				logger: context.logger,
				fallbackModelConfig: context.modelId,
			});

			if (buildOutcome.nodeSimulationPlan === undefined) {
				return await handleMissingSimulationPlan({
					input: resolvedInput,
					context,
					workflowTaskService,
					workflowId,
				});
			}

			const preparedResult = prepareVerificationRun(buildOutcome, resolvedInput);
			if (preparedResult.kind === 'blocked') {
				return {
					...preparedResult.result,
					resolvedWorkItemId: resolvedInput.workItemId,
				};
			}
			const { prepared } = preparedResult;

			const chatModelRecovery = await target.domainContext.workflowService
				.getAsWorkflowJSON(workflowId)
				.then(
					async (workflow) =>
						await collectChatModelRecoveryContext(
							target.domainContext,
							workflow.nodes ?? [],
							workflow.connections,
						),
				)
				.catch(() => undefined);
			const chatModelRelatedNodeNames = chatModelRecovery?.relatedNodeNames;

			// A scripted gate replaces the halt with one loop-safe pass per decision;
			// otherwise run the single standard pass (halted gates pin zero items).
			const { result, analysis } = prepared.gateScript
				? await runScriptedGateVerification({
						script: prepared.gateScript,
						prepared,
						executionService: target.domainContext.executionService,
						workflowId,
						inputData: resolvedInput.inputData,
						triggerNodeName: resolvedInput.triggerNodeName,
						timeout: resolvedInput.timeout,
						abortSignal: context.abortSignal,
						buildOutcome,
						stateBefore: target.stateBefore,
						runId: context.runId,
						chatModelRelatedNodeNames,
						chatModelRecovery,
					})
				: await (async () => {
						const runResult = await target.domainContext.executionService.run(
							workflowId,
							resolvedInput.inputData,
							{
								timeout: resolvedInput.timeout,
								triggerNodeName: resolvedInput.triggerNodeName,
								verificationPinData: prepared.verificationPinData,
								isVerificationRun: true,
								abortSignal: context.abortSignal,
							},
						);
						return {
							result: runResult,
							analysis: analyzeVerificationResult({
								result: runResult,
								buildOutcome,
								simulatedNodes: prepared.simulatedNodes,
								haltedGateNames: prepared.haltedGateNames,
								triggerNodeName: resolvedInput.triggerNodeName,
								stateBefore: target.stateBefore,
								runId: context.runId,
								chatModelRelatedNodeNames,
								chatModelRecovery,
							}),
						};
					})();

			await persistVerificationOutcome({
				input: resolvedInput,
				context,
				workflowTaskService,
				workflowId,
				result,
				analysis,
				verifyAttempts: (buildOutcome.verifyAttempts ?? 0) + 1,
			});

			const maxDataChars = resolvedInput.maxDataChars ?? DEFAULT_NODE_PREVIEW_CHARS;
			const simulatedNames = new Set(analysis.reachedSimulatedNodes.map((n) => n.nodeName));
			return {
				resolvedWorkItemId: resolvedInput.workItemId,
				executionId: result.executionId || undefined,
				success: analysis.success,
				status: result.status,
				nodesExecuted: analysis.nodesExecuted,
				lastNodeExecuted: result.lastNodeExecuted,
				nodePreviews: buildNodePreviews(result.data, maxDataChars, simulatedNames),
				simulatedNodes:
					analysis.reachedSimulatedNodes.length > 0 ? analysis.reachedSimulatedNodes : undefined,
				simulationNote: analysis.simulationNote,
				nodeErrors: analysis.nodeErrors.length > 0 ? analysis.nodeErrors : undefined,
				nodesNotReached: analysis.nodesNotReached.length > 0 ? analysis.nodesNotReached : undefined,
				coverageNote: analysis.coverageNote,
				...(resolvedInput.includeData ? { data: result.data } : {}),
				error: analysis.errorMessage,
				remediation: analysis.remediation,
				guidance: analysis.remediation?.guidance,
			};
		})
		.build();
}
