/**
 * complete-checkpoint tool — called by the orchestrator to settle a planned-task
 * checkpoint.
 *
 * The service enqueues an internal follow-up run carrying a checkpoint's spec.
 * The orchestrator executes the spec using its normal tools (verify-built-workflow,
 * executions(action="run"), etc.) and then MUST call this tool exactly once to
 * report the outcome. The service's post-run deadlock fallback guarantees
 * progress even if the orchestrator forgets.
 */

import { Tool } from '@n8n/agents';
import { isRecord } from '@n8n/utils/is-record';
import { z } from 'zod';

import type { OrchestrationContext } from '../../types';
import {
	getSkippedSetupSubjects,
	partitionSkippedSetupRequests,
} from '../workflows/setup-skip-state';
import { analyzeWorkflow } from '../workflows/setup-workflow.service';

const inputSchema = z.object({
	taskId: z.string().describe('The checkpoint task ID from the <planned-task-follow-up> payload'),
	status: z
		.enum(['succeeded', 'failed'])
		.describe('Whether the verification passed (succeeded) or failed (failed)'),
	result: z
		.string()
		.optional()
		.describe('Short user-visible note describing the verification outcome'),
	error: z.string().optional().describe('Error message when status=failed'),
	outcome: z
		.record(z.unknown())
		.optional()
		.describe('Structured outcome payload (e.g., executionId, failureNode, data excerpt)'),
});

const outputSchema = z.object({
	result: z.string(),
	ok: z.boolean(),
});

function requiresWorkflowSetup(outcome: Record<string, unknown> | undefined): boolean {
	const setupRequirement = outcome?.setupRequirement;
	return isRecord(setupRequirement) && setupRequirement.status === 'required';
}

function getWorkflowId(outcome: Record<string, unknown> | undefined): string | undefined {
	const workflowId = outcome?.workflowId;
	return typeof workflowId === 'string' && workflowId.length > 0 ? workflowId : undefined;
}

/** Nodes the build changed — pending setup on other (pre-existing) nodes must not gate the checkpoint. */
function getChangedNodeNames(outcome: Record<string, unknown> | undefined): string[] | undefined {
	const changedNodeNames = outcome?.changedNodeNames;
	if (!Array.isArray(changedNodeNames)) return undefined;
	return changedNodeNames.filter((name): name is string => typeof name === 'string');
}

async function rejectIfSetupStillRequired(
	context: OrchestrationContext,
	checkpointTaskId: string,
): Promise<{ ok: true; skippedNote?: string } | { ok: false; result: string }> {
	const graph = await context.plannedTaskService?.getGraph(context.threadId);
	if (!graph) return { ok: true };

	const checkpoint = graph.tasks.find((task) => task.id === checkpointTaskId);
	if (!checkpoint || checkpoint.kind !== 'checkpoint') return { ok: true };
	if (checkpoint.status !== 'running') return { ok: true };

	const dependentWorkflows = graph.tasks
		.filter((task) => checkpoint.deps.includes(task.id))
		.filter((task) => task.kind === 'build-workflow' && requiresWorkflowSetup(task.outcome))
		.map((task) => ({
			workflowId: getWorkflowId(task.outcome),
			changedNodeNames: getChangedNodeNames(task.outcome),
		}))
		.filter(
			(entry): entry is { workflowId: string; changedNodeNames: string[] | undefined } =>
				entry.workflowId !== undefined,
		);

	if (dependentWorkflows.length === 0) return { ok: true };

	const domainContext = context.domainContext;
	if (!domainContext) {
		return {
			ok: false,
			result:
				'Error: checkpoint cannot be completed yet because workflow setup is still required, ' +
				'but the workflow context is unavailable. Call workflows(action="setup") before complete-checkpoint.',
		};
	}

	const skippedNotes: string[] = [];

	for (const { workflowId, changedNodeNames } of dependentWorkflows) {
		try {
			const setupRequests = await analyzeWorkflow(domainContext, workflowId);
			// Credentials the user skipped can never be satisfied by another setup call, so
			// counting them here would deadlock the checkpoint against a card that must not reopen.
			const { pending, skippedByUser } = partitionSkippedSetupRequests(
				setupRequests,
				workflowId,
				getSkippedSetupSubjects(domainContext),
			);
			// Nodes this build never touched don't gate the checkpoint either, and are not worth
			// reporting: the user was never asked about them.
			const blocks = (request: (typeof setupRequests)[number]) =>
				request.needsAction === true &&
				(!changedNodeNames || changedNodeNames.includes(request.node.name));
			const pendingRequests = pending.filter(blocks);
			// Passing the guard silently would let the checkpoint report a workflow as done with
			// no mention that parts of it can't run — the one thing the user needs to hear.
			const skipped = skippedByUser.filter(blocks);
			if (skipped.length > 0) {
				const described = skipped
					.map((request) =>
						request.credentialType
							? `${request.node.name} (${request.credentialType})`
							: request.node.name,
					)
					.join(', ');
				skippedNotes.push(`workflow "${workflowId}": ${described}`);
			}
			if (pendingRequests.length > 0) {
				const nodeNames = pendingRequests
					.map((request) => request.node.name)
					.filter((name): name is string => typeof name === 'string' && name.length > 0);
				const suffix = nodeNames.length > 0 ? ` Pending setup nodes: ${nodeNames.join(', ')}.` : '';
				return {
					ok: false,
					result:
						`Error: workflow setup is still required for workflow "${workflowId}". ` +
						`Call workflows(action="setup", workflowId="${workflowId}") before complete-checkpoint.` +
						suffix,
				};
			}
		} catch (error) {
			return {
				ok: false,
				result:
					`Error: workflow setup could not be checked for workflow "${workflowId}": ` +
					`${error instanceof Error ? error.message : String(error)}. ` +
					`Call workflows(action="setup", workflowId="${workflowId}") before complete-checkpoint.`,
			};
		}
	}

	if (skippedNotes.length > 0) {
		return {
			ok: true,
			skippedNote:
				`Setup the user skipped earlier is still outstanding — ${skippedNotes.join('; ')}. ` +
				'Do not re-open the setup card. Say in your summary which parts stay unconfigured and ' +
				'what that means when the workflow runs.',
		};
	}

	return { ok: true };
}

export function createCompleteCheckpointTool(context: OrchestrationContext) {
	return new Tool('complete-checkpoint')
		.description(
			'Report the outcome of a planned-task checkpoint you just executed. Only call in checkpoint follow-up turns. ' +
				'Call this exactly once per <planned-task-follow-up type="checkpoint"> block. ' +
				'Only valid for tasks of kind "checkpoint" that are currently running; ' +
				'calling with any other taskId returns an error and does not modify the graph.',
		)
		.input(inputSchema)
		.output(outputSchema)
		.handler(async (input: z.infer<typeof inputSchema>) => {
			if (!context.plannedTaskService) {
				return { ok: false, result: 'Error: planned task service not available.' };
			}

			let skippedNote: string | undefined;
			if (input.status === 'succeeded') {
				const setupGuard = await rejectIfSetupStillRequired(context, input.taskId);
				if (!setupGuard.ok) return setupGuard;
				skippedNote = setupGuard.skippedNote;
			}

			const settleResult =
				input.status === 'succeeded'
					? await context.plannedTaskService.markCheckpointSucceeded(
							context.threadId,
							input.taskId,
							{
								result: input.result,
								outcome: input.outcome,
							},
						)
					: await context.plannedTaskService.markCheckpointFailed(context.threadId, input.taskId, {
							error: input.error ?? input.result ?? 'Checkpoint verification failed',
							// Preserve structured outcome (executionId, failureNode, data excerpt).
							// Without this, replans only see a flat error string and lose execution
							// context that would otherwise seed a targeted retry.
							outcome: input.outcome,
						});

			if (settleResult.ok) {
				return {
					ok: true,
					result:
						`Checkpoint ${input.taskId} marked ${input.status}.` +
						(skippedNote ? ` ${skippedNote}` : ''),
				};
			}

			const reason = settleResult.reason;
			if (reason === 'not-found') {
				return {
					ok: false,
					result: `Error: no task with id "${input.taskId}" exists in the current plan.`,
				};
			}
			if (reason === 'wrong-kind') {
				return {
					ok: false,
					result:
						`Error: task "${input.taskId}" is not a checkpoint ` +
						`(actual kind: ${settleResult.actual?.kind ?? 'unknown'}). ` +
						'complete-checkpoint can only settle checkpoint tasks.',
				};
			}
			return {
				ok: false,
				result:
					`Error: checkpoint "${input.taskId}" is not in running state ` +
					`(actual status: ${settleResult.actual?.status ?? 'unknown'}).`,
			};
		})
		.build();
}
