import type { User } from '@n8n/db';
import {
	needsPinData,
	discoverOutputSchemaForNode,
	inferSchemasFromRunData,
	type JsonSchema,
} from '@n8n/workflow-sdk';
import type { Logger } from '@n8n/backend-common';
import type { INodeExecutionData } from 'n8n-workflow';
import { isTriggerNode, jsonStringify } from 'n8n-workflow';
import z from 'zod';

import { USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../mcp.types';
import { getMcpWorkflow } from './workflow-validation.utils';

import type { ExecutionService } from '@/executions/execution.service';
import type { NodeTypes } from '@/node-types';
import type { Telemetry } from '@/telemetry';
import type { WorkflowFinderService } from '@/workflows/workflow-finder.service';

const inputSchema = z.object({
	workflowId: z.string().describe('The ID of the workflow to generate test pin data for'),
});

const outputSchema = {
	nodeSchemasToGenerate: z
		.record(z.record(z.unknown()))
		.describe(
			'Nodes that need pin data generated by you. Keys are node names, values are JSON Schema objects describing the expected output shape. Schemas may come from node type definitions or inferred from past execution output shapes. Generate realistic sample data matching each schema, wrap each item as {"json": {...}}, and collect into a pinData object to pass to test_workflow.',
		),
	nodesWithoutSchema: z
		.array(z.string())
		.describe(
			'Node names that need pin data but have no output schema. Generate a single item [{"json": {}}] for each, and merge into pinData before passing to test_workflow.',
		),
	nodesSkipped: z
		.array(z.string())
		.describe(
			'Nodes that do not need pin data and will execute normally during the test (e.g. Set, If, Code, Merge).',
		),
	coverage: z.object({
		withSchemaFromExecution: z
			.number()
			.describe('Number of nodes with schemas inferred from last successful execution output'),
		withSchemaFromDefinition: z
			.number()
			.describe('Number of nodes with schemas from node type definitions'),
		withoutSchema: z
			.number()
			.describe('Number of nodes with no data or schema — use empty defaults'),
		skipped: z.number().describe('Number of nodes that will execute normally (no pin data needed)'),
		total: z.number().describe('Total number of enabled nodes'),
	}),
} satisfies z.ZodRawShape;

export const createPrepareTestPinDataTool = (
	user: User,
	workflowFinderService: WorkflowFinderService,
	executionService: ExecutionService,
	nodeTypes: NodeTypes,
	telemetry: Telemetry,
	logger: Logger,
): ToolDefinition<typeof inputSchema.shape> => ({
	name: 'prepare_test_pin_data',
	config: {
		description:
			'Prepare test pin data for a workflow. Trigger nodes, nodes with credentials, and HTTP Request nodes need pin data. Logic nodes (Set, If, Code, etc.) and credential-free I/O nodes (Execute Command, file read/write) execute normally without pin data. Returns JSON Schemas describing the expected output shape for each node that needs pin data — schemas are derived from past execution output shapes or node type definitions. No actual user data is returned. You should generate realistic sample data for the schemas, use empty defaults for nodes without schema, merge everything into a single pinData object, and pass it to test_workflow.',
		inputSchema: inputSchema.shape,
		outputSchema,
		annotations: {
			title: 'Prepare Test Pin Data',
			readOnlyHint: true,
			destructiveHint: false,
			idempotentHint: true,
			openWorldHint: false,
		},
	},
	handler: async ({ workflowId }: z.infer<typeof inputSchema>) => {
		const telemetryPayload: UserCalledMCPToolEventPayload = {
			user_id: user.id,
			tool_name: 'prepare_test_pin_data',
			parameters: { workflowId },
		};

		try {
			const result = await preparePinData(
				workflowId,
				user,
				workflowFinderService,
				executionService,
				nodeTypes,
				logger,
			);

			telemetryPayload.results = {
				success: true,
				data: result.coverage,
			};
			telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);

			return {
				content: [{ type: 'text', text: jsonStringify(result) }],
				structuredContent: { ...result },
			};
		} catch (error) {
			const errorMessage = error instanceof Error ? error.message : String(error);
			telemetryPayload.results = { success: false, error: errorMessage };
			telemetry.track(USER_CALLED_MCP_TOOL_EVENT, telemetryPayload);

			return {
				content: [{ type: 'text', text: jsonStringify({ error: errorMessage }) }],
				structuredContent: { error: errorMessage },
				isError: true,
			};
		}
	},
});

// =============================================================================
// Core Logic
// =============================================================================

interface PreparePinDataResult {
	nodeSchemasToGenerate: Record<string, JsonSchema>;
	nodesWithoutSchema: string[];
	nodesSkipped: string[];
	coverage: {
		withSchemaFromExecution: number;
		withSchemaFromDefinition: number;
		withoutSchema: number;
		skipped: number;
		total: number;
	};
}

export async function preparePinData(
	workflowId: string,
	user: User,
	workflowFinderService: WorkflowFinderService,
	executionService: ExecutionService,
	nodeTypes: NodeTypes,
	logger: Logger,
): Promise<PreparePinDataResult> {
	const workflow = await getMcpWorkflow(workflowId, user, ['workflow:read'], workflowFinderService);

	const enabledNodes = (workflow.nodes ?? []).filter((n) => !n.disabled);

	// Build trigger detection callback using the NodeTypes registry
	const isTriggerNodeFn = (node: { type: string; typeVersion: number }) => {
		try {
			const nodeType = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
			return isTriggerNode(nodeType.description);
		} catch {
			// Unknown node type — safer to pin it
			return true;
		}
	};

	// Tier 1: Try to infer schemas from last successful execution output
	const executionRunData = await getExecutionRunData(workflowId, user, executionService, logger);
	const executionSchemas = executionRunData ? inferSchemasFromRunData(executionRunData) : {};

	const nodeSchemasToGenerate: Record<string, JsonSchema> = {};
	const nodesWithoutSchema: string[] = [];
	const nodesSkipped: string[] = [];

	let withSchemaFromExecution = 0;
	let withSchemaFromDefinition = 0;
	let withoutSchema = 0;
	let skipped = 0;

	for (const node of enabledNodes) {
		// Check if this node needs pin data at all
		if (!needsPinData(node, isTriggerNodeFn)) {
			nodesSkipped.push(node.name);
			skipped++;
			continue;
		}

		// Tier 1: Infer schema from execution history output
		const execSchema = executionSchemas[node.name];
		if (execSchema) {
			nodeSchemasToGenerate[node.name] = execSchema;
			withSchemaFromExecution++;
			continue;
		}

		// Tier 2: Schema from node type definition
		const schema = discoverOutputSchemaForNode(node.type, node.typeVersion, {
			resource: node.parameters?.resource as string | undefined,
			operation: node.parameters?.operation as string | undefined,
		});
		if (schema) {
			nodeSchemasToGenerate[node.name] = schema;
			withSchemaFromDefinition++;
			continue;
		}

		// Tier 3: No data, no schema
		nodesWithoutSchema.push(node.name);
		withoutSchema++;
	}

	return {
		nodeSchemasToGenerate,
		nodesWithoutSchema,
		nodesSkipped,
		coverage: {
			withSchemaFromExecution,
			withSchemaFromDefinition,
			withoutSchema,
			skipped,
			total: enabledNodes.length,
		},
	};
}

// =============================================================================
// Execution Data Fetching (backend-specific)
// =============================================================================

async function getExecutionRunData(
	workflowId: string,
	user: User,
	executionService: ExecutionService,
	logger: Logger,
): Promise<Record<string, INodeExecutionData[]> | undefined> {
	try {
		const execution = await executionService.getLastSuccessfulExecution(workflowId, user);
		if (!execution?.data?.resultData?.runData) return undefined;

		const result: Record<string, INodeExecutionData[]> = {};
		for (const [nodeName, taskDataArray] of Object.entries(execution.data.resultData.runData)) {
			const firstRun = taskDataArray[0];
			if (firstRun?.data?.main?.[0]) {
				result[nodeName] = firstRun.data.main[0];
			}
		}

		return Object.keys(result).length > 0 ? result : undefined;
	} catch (error) {
		logger.debug('Failed to fetch execution data for pin data generation', {
			workflowId,
			error: error instanceof Error ? error.message : String(error),
		});
		return undefined;
	}
}
