import type {
	IConnections,
	INode,
	IWorkflowGroup,
	IWorkflowSettings,
	WorkflowFEMeta,
} from 'n8n-workflow';
import z from 'zod';

export const nodeCredentialSummarySchema = z
	.object({
		id: z
			.string()
			.describe('The credential ID, used to reference this credential in write operations'),
		name: z.string().describe('The credential name'),
	})
	.describe('The credential assigned to this slot (id and name only, never secret data)');

export const nodeSchema = z
	.object({
		name: z.string(),
		type: z.string(),
		credentials: z
			.record(z.string(), nodeCredentialSummarySchema)
			.optional()
			.describe(
				'Credentials assigned to the node, keyed by credential type (e.g. "slackApi"). Reuse these when adding nodes for the same service to this workflow.',
			),
	})
	.passthrough();

/**
 * Reduces a node's credentials to `{ id, name }` per slot for the read path,
 * keeping only slots a client can reference by id when reusing them. Any slot
 * with no id is dropped; in practice those are AI Gateway synthetic sentinels
 * (`__aiGatewayManaged`, always `id: null`), which have no id to reference and
 * whose marker is stripped here, leaving them indistinguishable from a real
 * credential. DB-backed managed credentials (`isManaged`) keep a real id and
 * are retained.
 */
export const sanitizeNodeCredentials = ({ credentials, ...node }: INode) => {
	const referenceable: Array<[string, { id: string; name: string }]> = [];
	for (const [type, cred] of Object.entries(credentials ?? {})) {
		if (!cred?.id) continue;
		referenceable.push([type, { id: cred.id, name: cred.name }]);
	}
	if (referenceable.length === 0) return node;
	return { ...node, credentials: Object.fromEntries(referenceable) };
};

export const connectionsSchema = z
	.custom<IConnections>((_value): _value is IConnections => true)
	.describe('The node connections, keyed by source node name');

export const tagSchema = z.object({ id: z.string(), name: z.string() }).passthrough();

export const toTagSummary = (tags: Array<{ id: string; name: string }> | undefined | null) =>
	(tags ?? []).map((tag) => ({ id: tag.id, name: tag.name }));

export const workflowSettingsSchema = z
	.custom<IWorkflowSettings>((_value): _value is IWorkflowSettings => true)
	.nullable();

export const workflowMetaSchema = z
	.custom<WorkflowFEMeta>((_value): _value is WorkflowFEMeta => true)
	.nullable();

export const dataTableColumnTypeSchema = z
	.enum(['string', 'number', 'boolean', 'date'])
	.describe('The data type of the column');

export const dataTableColumnSchema = z.object({
	id: z.string().describe('The unique identifier of the column'),
	name: z.string().describe('The name of the column'),
	type: dataTableColumnTypeSchema,
	index: z.number().int().describe('The position of the column in the table'),
});

export const dataTableSchema = z.object({
	id: z.string().describe('Autogenerated unique identifier of the data table.'),
	name: z.string().describe('The name of the data table'),
	projectId: z.string().describe('The project this data table belongs to'),
	createdAt: z.string().describe('Autogenerated ISO timestamp when the data table was created'),
	updatedAt: z
		.string()
		.describe('Autogenerated ISO timestamp when the data table was last updated'),
	columns: z.array(dataTableColumnSchema).describe('The columns defined in this data table'),
});

export const createLimitSchema = (max: number) =>
	z
		.number()
		.int()
		.positive()
		.max(max)
		.optional()
		.describe(`Limit the number of results (max ${max})`);

export const dataTableProjectIdSchema = z
	.string()
	.describe('The project ID the data table belongs to');

export const columnNameSchema = z
	.string()
	.min(1)
	.max(63)
	.regex(/^[a-zA-Z][a-zA-Z0-9_]*$/)
	.describe(
		'Column name. Must start with a letter, contain only letters, numbers, and underscores (max 63 chars)',
	);

export const folderOutputSchema = {
	id: z.string().optional().describe('The ID of the folder'),
	name: z.string().optional().describe('The name of the folder'),
	parentFolderId: z
		.string()
		.nullable()
		.optional()
		.describe('The ID of the parent folder, or null if the folder is at the project root'),
	error: z
		.string()
		.optional()
		.describe('Error message explaining why the operation failed. Present only on failure.'),
} satisfies z.ZodRawShape;

export const dataTableRowSchema = z.record(
	z.string(),
	z.union([z.string(), z.number(), z.boolean(), z.null()]),
);

export const successMessageOutputSchema = {
	success: z.boolean().describe('Whether the operation succeeded'),
	message: z.string().describe('Description of the result'),
} satisfies z.ZodRawShape;

export const nodeGroupSchema = z
	.object({
		id: z.string(),
		name: z.string(),
		nodeNames: z
			.array(z.string())
			.describe('Names of the member nodes, matching the update_workflow group operations.'),
		description: z.string().optional(),
	})
	.describe('A named visual grouping of nodes');

export type NodeGroupSummary = z.infer<typeof nodeGroupSchema>;

/**
 * Maps persisted node groups (which store member node *ids*) to the read-path
 * shape, which presents member node *names* — the same contract the
 * update_workflow group operations accept. Stale ids that no longer resolve to
 * a node are dropped, so echoing a group back into a write op repairs it.
 */
export const toNodeGroupSummary = (
	nodeGroups: IWorkflowGroup[],
	nodes: Array<Pick<INode, 'id' | 'name'>>,
): NodeGroupSummary[] => {
	const nameById = new Map(nodes.map((node) => [node.id, node.name]));
	return nodeGroups.map(({ id, name, nodeIds, description }) => ({
		id,
		name,
		nodeNames: nodeIds.flatMap((nodeId) => nameById.get(nodeId) ?? []),
		...(description !== undefined ? { description } : {}),
	}));
};

const FULL_DETAIL_ONLY_NOTE = "Only included when detailLevel is 'full'.";

export const workflowDetailsOutputSchema = z.object({
	workflow: z
		.object({
			id: z.string(),
			name: z.string().nullable(),
			active: z.boolean(),
			isArchived: z.boolean(),
			versionId: z.string().describe('The current workflow version ID'),
			activeVersionId: z
				.string()
				.nullable()
				.describe('The active workflow version ID, if available'),
			triggerCount: z.number(),
			nodeCount: z
				.number()
				.describe('Number of nodes in the workflow, reported even when detailLevel omits them'),
			createdAt: z.string().nullable(),
			updatedAt: z.string().nullable(),
			settings: workflowSettingsSchema,
			connections: z.record(z.unknown()).optional().describe(FULL_DETAIL_ONLY_NOTE),
			nodes: z.array(nodeSchema).optional().describe(FULL_DETAIL_ONLY_NOTE),
			nodeGroups: z
				.array(nodeGroupSchema)
				.optional()
				.describe(`Node groups in the workflow. ${FULL_DETAIL_ONLY_NOTE}`),
			activeVersion: z
				.discriminatedUnion('sameAsDraft', [
					z.object({
						sameAsDraft: z
							.literal(true)
							.describe(
								'The published version is identical to the current draft — use the top-level nodes, connections and nodeGroups.',
							),
					}),
					z.object({
						sameAsDraft: z.literal(false),
						nodes: z.array(nodeSchema),
						connections: z.record(z.unknown()),
						nodeGroups: z.array(nodeGroupSchema).describe('Node groups in the active version'),
					}),
				])
				.nullable()
				.optional()
				.describe(
					`Published (active) workflow graph. Null when the workflow has no published version. ${FULL_DETAIL_ONLY_NOTE}`,
				),
			tags: z.array(tagSchema),
			meta: workflowMetaSchema.optional().describe(FULL_DETAIL_ONLY_NOTE),
			parentFolderId: z.string().nullable(),
			description: z.string().optional().describe('The description of the workflow'),
			scopes: z.array(z.string()).describe('User permissions for this workflow'),
			canExecute: z.boolean().describe('Whether the user has permission to execute this workflow'),
		})
		.passthrough()
		.describe('Sanitized workflow data safe for MCP consumption'),
	triggerInfo: z
		.string()
		.describe(
			'Human-readable instructions describing how to trigger the workflow, based on the current draft (what manual executions run)',
		),
	activeVersionTriggerInfo: z
		.string()
		.optional()
		.describe(
			"Trigger info for the published (active) version, which production executions via execute_workflow run. Only present when it differs from the draft's triggerInfo.",
		),
});
