import type { ListQueryDb, ScopesField, User } from '@n8n/db';
import z from 'zod';

import type { CredentialsService } from '@/credentials/credentials.service';
import type { ListQuery } from '@/requests';
import type { AiGatewayService } from '@/services/ai-gateway.service';
import type { Telemetry } from '@/telemetry';

import { toN8nConnectCoverage } from '../mcp-ai-gateway.helper';
import { LIST_N8N_GATEWAY_SERVICES_TOOL_NAME, USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import type {
	N8nConnectCoverage,
	ToolDefinition,
	UserCalledMCPToolEventPayload,
} from '../mcp.types';
import { createLimitSchema } from './schemas';

const MAX_RESULTS = 200;

const inputSchema = {
	limit: createLimitSchema(MAX_RESULTS),
	query: z.string().optional().describe('Filter credentials by name (partial match)'),
	type: z
		.string()
		.optional()
		.describe('Filter by credential type (e.g. "slackApi", "httpHeaderAuth"). Partial match.'),
	projectId: z
		.string()
		.optional()
		.describe('Restrict results to credentials belonging to this project'),
	onlySharedWithMe: z
		.boolean()
		.optional()
		.describe('Only return credentials shared directly with the current user'),
} satisfies z.ZodRawShape;

const homeProjectSchema = z
	.object({
		id: z.string().describe('The unique identifier of the project'),
		name: z.string().describe('The name of the project'),
		type: z
			.string()
			.describe(
				"The project type. 'personal' is a user's private project; 'team' is a shared project accessible to multiple users.",
			),
	})
	.nullable()
	.describe('The project that owns the credential, if available');

const n8nConnectSchema = z
	.object({
		credentialTypes: z
			.array(z.string())
			.describe('Credential type names that Gateway credits can provide (e.g. "openAiApi").'),
		nodes: z
			.array(z.string())
			.describe('Node types covered by Gateway credits (e.g. "@n8n/n8n-nodes-langchain.openAi").'),
	})
	.optional()
	.describe(
		`Present when Gateway credits are available for this instance. Omitted otherwise. Candidate coverage only — actual eligibility for a managed credential also depends on the node action, minimum type version, and hidden properties; call ${LIST_N8N_GATEWAY_SERVICES_TOOL_NAME} for the authoritative contract.`,
	);

const outputSchema = {
	data: z
		.array(
			z.object({
				id: z.string().describe('The unique identifier of the credential'),
				name: z.string().describe('The name of the credential'),
				type: z.string().describe('The credential type (e.g. "slackApi")'),
				scopes: z
					.array(z.string())
					.describe('The user permissions on this credential (e.g. "credential:read")'),
				isManaged: z
					.boolean()
					.describe('Whether the credential is managed by n8n and cannot be edited by the user'),
				isGlobal: z.boolean().describe('Whether the credential is available to all users'),
				homeProject: homeProjectSchema,
			}),
		)
		.describe('List of credentials accessible to the current user'),
	count: z.number().int().min(0).describe('Number of credentials returned'),
	gatewayCredits: n8nConnectSchema,
	error: z.string().optional().describe('Error message when the tool failed'),
} satisfies z.ZodRawShape;

export type ListCredentialsParams = {
	limit?: number;
	query?: string;
	type?: string;
	projectId?: string;
	onlySharedWithMe?: boolean;
};

export type ListCredentialsItem = {
	id: string;
	name: string;
	type: string;
	scopes: string[];
	isManaged: boolean;
	isGlobal: boolean;
	homeProject: { id: string; name: string; type: string } | null;
};

export type ListCredentialsResult = {
	data: ListCredentialsItem[];
	count: number;
	gatewayCredits?: N8nConnectCoverage;
	error?: string;
};

export const createListCredentialsTool = (
	user: User,
	credentialsService: CredentialsService,
	telemetry: Telemetry,
	aiGatewayService: AiGatewayService,
): ToolDefinition<typeof inputSchema> => ({
	name: 'list_credentials',
	config: {
		description:
			"List credentials the current user can access. Use this to find a credential ID before referencing it anywhere one is required. Prefer reusing a credential already used by another node in the workflow (get_workflow_details with detailLevel 'full' shows the credentials on each node); when the workflow has none of that type and multiple candidates exist, ask the user which one to use rather than picking one. Never returns credential secret data.",
		inputSchema,
		outputSchema,
		annotations: {
			title: 'List Credentials',
			readOnlyHint: true,
			destructiveHint: false,
			idempotentHint: true,
			openWorldHint: false,
		},
	},
	handler: async ({
		limit = MAX_RESULTS,
		query,
		type,
		projectId,
		onlySharedWithMe = false,
	}: ListCredentialsParams) => {
		const telemetryPayload: UserCalledMCPToolEventPayload = {
			user_id: user.id,
			tool_name: 'list_credentials',
			parameters: { limit, query, type, projectId, onlySharedWithMe },
		};

		try {
			const payload = await listCredentials(user, credentialsService, {
				limit,
				query,
				type,
				projectId,
				onlySharedWithMe,
			});

			const coverage = toN8nConnectCoverage(await aiGatewayService.isAvailable());
			if (coverage) payload.gatewayCredits = coverage;

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

			return {
				content: [{ type: 'text', text: JSON.stringify(payload) }],
				structuredContent: payload,
			};
		} 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);

			const output: ListCredentialsResult = {
				data: [],
				count: 0,
				error: errorMessage,
			};
			return {
				content: [{ type: 'text', text: JSON.stringify(output) }],
				structuredContent: output,
				isError: true,
			};
		}
	},
});

export async function listCredentials(
	user: User,
	credentialsService: CredentialsService,
	{ limit = MAX_RESULTS, query, type, projectId, onlySharedWithMe = false }: ListCredentialsParams,
): Promise<ListCredentialsResult> {
	const safeLimit = Math.min(Math.max(1, limit), MAX_RESULTS);

	const filter: ListQuery.Options['filter'] = {
		...(query ? { name: query } : {}),
		...(type ? { type } : {}),
		...(projectId ? { projectId } : {}),
	};

	const listQueryOptions: ListQuery.Options = {
		take: safeLimit,
		...(Object.keys(filter ?? {}).length ? { filter } : {}),
	};

	const credentials = await credentialsService.getMany(user, {
		listQueryOptions,
		includeScopes: true,
		includeData: false,
		includeGlobal: !onlySharedWithMe,
		onlySharedWithMe,
	});

	const enriched = credentials as Array<
		ListQueryDb.Credentials.WithOwnedByAndSharedWith & ScopesField
	>;

	const data: ListCredentialsItem[] = enriched.map((c) => ({
		id: c.id,
		name: c.name,
		type: c.type,
		scopes: c.scopes ?? [],
		isManaged: c.isManaged,
		isGlobal: c.isGlobal,
		homeProject: c.homeProject
			? { id: c.homeProject.id, name: c.homeProject.name, type: c.homeProject.type }
			: null,
	}));

	return { data, count: data.length };
}
