import { subAgentToolRelations, tools } from "../../db/manage/manage-schema.js";
import { CredentialStoreType, MCPServerType, MCPTransportType } from "../../types/utility.js";
import { detectAuthenticationRequired } from "../../utils/auth-detection.js";
import { env } from "../../env.js";
import { getLogger } from "../../utils/logger.js";
import { convertZodToJsonSchema } from "../../utils/schema-conversion.js";
import { configureComposioMCPServer } from "../../utils/third-party-mcp-servers/composio-client.js";
import { isThirdPartyMCPServerAuthenticated } from "../../utils/third-party-mcp-servers/third-party-check.js";
import { getCredentialStoreLookupKeyFromRetrievalParams } from "../../utils/credential-store-utils.js";
import { CredentialStuffer } from "../../credential-stuffer/CredentialStuffer.js";
import "../../credential-stuffer/index.js";
import { createAgentsRunDatabaseClient } from "../../db/runtime/runtime-client.js";
import { deriveRelationId } from "../../utils/conversations.js";
import { getActiveBranch } from "../../dolt/schema-sync.js";
import { agentScopedWhere, projectScopedWhere } from "./scope-helpers.js";
import { isSlackWorkAppTool } from "../runtime/slack-work-app-mcp.js";
import { cascadeDeleteByTool } from "../runtime/cascade-delete.js";
import { updateAgentToolRelation } from "./subAgentRelations.js";
import { isSerializationError } from "../../retry/retryable-errors.js";
import { toISODateString } from "../../utils/date.js";
import { McpClient } from "../../utils/mcp-client.js";
import { TRUSTED_WORK_APP_MCP_PATHS, isTrustedWorkAppMcpUrl } from "../../utils/work-app-mcp.js";
import "../../utils/index.js";
import { unwrapJsonSchemaWrapper } from "../../utils/json-schema-walk.js";
import { isGithubWorkAppTool } from "../runtime/github-work-app-installations.js";
import { getCredentialReference, getUserScopedCredentialReference } from "./credentialReferences.js";
import { and, count, desc, eq } from "drizzle-orm";
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";

//#region src/data-access/manage/tools.ts
/**
* Check if an error is a timeout/connection error (transient, not auth-related).
* Uses MCP SDK ErrorCode for proper type safety.
*/
function isTimeoutOrConnectionError(error) {
	if (error instanceof McpError) return error.code === ErrorCode.RequestTimeout || error.code === ErrorCode.ConnectionClosed || error.code === ErrorCode.InternalError;
	if (error instanceof StreamableHTTPError) return error.code !== void 0 && error.code >= 500;
	if (error instanceof Error) {
		const message = error.message.toLowerCase();
		const cause = error.cause;
		if (message.includes("timed out") || message.includes("timeout") || message.includes("fetch failed")) return true;
		if (cause?.code && [
			"ETIMEDOUT",
			"ECONNABORTED",
			"ECONNRESET",
			"ECONNREFUSED",
			"ENOTFOUND",
			"EHOSTUNREACH",
			"ENETUNREACH",
			"EPIPE"
		].includes(cause.code)) return true;
	}
	return false;
}
/**
* Check if an error indicates the credential is invalid/expired/revoked.
* These errors mean the user genuinely needs to re-authenticate.
*/
function isAuthenticationError(error) {
	if (error instanceof UnauthorizedError) return true;
	if (error instanceof StreamableHTTPError) return error.code === 401 || error.code === 403;
	if (error instanceof Error) {
		const message = error.message.toLowerCase();
		if (message.includes("unauthorized") || message.includes("forbidden") || message.includes("invalid_token") || message.includes("token expired") || message.includes("invalid_grant")) return true;
	}
	return false;
}
const logger = getLogger("tools");
/**
* Extract expiration date from credential data stored in credential store
*/
async function getCredentialExpiresAt(credentialReference, credentialStoreRegistry) {
	if (!credentialReference.retrievalParams) return void 0;
	const credentialStore = credentialStoreRegistry?.get(credentialReference.credentialStoreId);
	if (!credentialStore || credentialStore.type === CredentialStoreType.memory) return void 0;
	const lookupKey = getCredentialStoreLookupKeyFromRetrievalParams({
		retrievalParams: credentialReference.retrievalParams,
		credentialStoreType: credentialStore.type
	});
	if (!lookupKey) return void 0;
	const credentialDataString = await credentialStore.get(lookupKey);
	if (!credentialDataString) return void 0;
	if (credentialStore.type === CredentialStoreType.nango) {
		const nangoCredentialData = JSON.parse(credentialDataString);
		return nangoCredentialData.expiresAt ? toISODateString(nangoCredentialData.expiresAt) : void 0;
	}
	if (credentialStore.type === CredentialStoreType.keychain) {
		const oauthTokens = JSON.parse(credentialDataString);
		return oauthTokens.expires_at ? toISODateString(oauthTokens.expires_at) : void 0;
	}
}
/**
* Extract input schema from MCP tool definition, handling multiple formats
* Different MCP servers may use different schema structures:
* - inputSchema (direct) - e.g., Notion MCP
* - parameters.properties - e.g., some other MCP servers
* - parameters (direct) - alternative format
* - schema - another possible location
*/
function extractInputSchema(toolDef, toolName, _toolOverrides) {
	return extractOriginalSchema(toolDef, toolName);
}
/**
* Normalize a tool's schema to plain JSON Schema for the manage UI. `McpClient.tools()`
* builds the schema as a Zod (via `z.fromJSONSchema`) or an AI SDK `jsonSchema()` wrapper;
* the UI needs JSON Schema so its shared walker can resolve $ref, unwrap nullables, expand
* union variants, and surface enums. Plain JSON Schema passes through unchanged.
*/
function toJsonSchemaForDisplay(raw, toolName) {
	if (!raw || typeof raw !== "object") return raw;
	const unwrapped = unwrapJsonSchemaWrapper(raw);
	if (unwrapped !== raw) return unwrapped;
	if (typeof raw.safeParse === "function" || typeof raw.parse === "function") try {
		return convertZodToJsonSchema(raw);
	} catch (error) {
		logger.warn({
			toolName,
			error: error instanceof Error ? error.message : String(error)
		}, "Failed to convert MCP tool schema to JSON Schema; tool will display no parameters");
		return {};
	}
	return raw;
}
function extractOriginalSchema(toolDef, toolName) {
	let raw;
	if (toolDef.inputSchema) raw = toolDef.inputSchema;
	else if (toolDef.parameters?.properties) raw = toolDef.parameters.properties;
	else if (toolDef.parameters && typeof toolDef.parameters === "object") raw = toolDef.parameters;
	else if (toolDef.schema) raw = toolDef.schema;
	else return {};
	return toJsonSchemaForDisplay(raw, toolName);
}
const convertToMCPToolConfig = (tool) => {
	if (tool.config.type !== "mcp") throw new Error(`Cannot convert non-MCP tool to MCP config: ${tool.id}`);
	return {
		id: tool.id,
		name: tool.name,
		description: tool.name,
		serverUrl: tool.config.mcp.server.url,
		mcpType: tool.config.mcp.server.url.includes("api.nango.dev") ? MCPServerType.nango : MCPServerType.generic,
		transport: tool.config.mcp.transport,
		headers: tool.headers,
		toolOverrides: tool.config.mcp.toolOverrides
	};
};
const discoverToolsFromServer = async (tool, credentialReference, credentialStoreRegistry, userId) => {
	if (tool.config.type !== "mcp") throw new Error(`Cannot discover tools from non-MCP tool: ${tool.id}`);
	try {
		let serverConfig;
		if (credentialReference) {
			const storeReference = {
				credentialStoreId: credentialReference.credentialStoreId,
				retrievalParams: credentialReference.retrievalParams || {}
			};
			if (!credentialStoreRegistry) throw new Error("CredentialStoreRegistry is required for authenticated tools");
			serverConfig = await new CredentialStuffer(credentialStoreRegistry).buildMcpServerConfig({
				tenantId: tool.tenantId,
				projectId: tool.projectId
			}, convertToMCPToolConfig(tool), storeReference);
		} else if ((tool.config.mcp.transport?.type || MCPTransportType.streamableHttp) === MCPTransportType.sse) serverConfig = {
			type: MCPTransportType.sse,
			url: tool.config.mcp.server.url,
			eventSourceInit: tool.config.mcp.transport?.eventSourceInit
		};
		else serverConfig = {
			type: MCPTransportType.streamableHttp,
			url: tool.config.mcp.server.url,
			requestInit: tool.config.mcp.transport?.requestInit,
			eventSourceInit: tool.config.mcp.transport?.eventSourceInit,
			reconnectionOptions: tool.config.mcp.transport?.reconnectionOptions,
			sessionId: tool.config.mcp.transport?.sessionId
		};
		const composioConnectedAccountId = credentialReference?.retrievalParams?.connectedAccountId;
		if (composioConnectedAccountId) configureComposioMCPServer(serverConfig, tool.tenantId, tool.projectId, tool.credentialScope === "user" ? "user" : "project", userId, composioConnectedAccountId);
		else if (serverConfig.url?.toString().includes("composio.dev")) logger.warn({
			toolName: tool.name,
			toolId: tool.id
		}, "Composio tool missing connectedAccountId — skipping auth injection to prevent credential leakage");
		const urlString = String(serverConfig.url);
		if (isGithubWorkAppTool(tool) && isTrustedWorkAppMcpUrl(urlString, TRUSTED_WORK_APP_MCP_PATHS.github, env.INKEEP_AGENTS_API_URL)) serverConfig.headers = {
			...serverConfig.headers,
			"x-inkeep-tool-id": tool.id,
			"x-inkeep-tenant-id": tool.tenantId,
			"x-inkeep-project-id": tool.projectId,
			Authorization: `Bearer ${env.GITHUB_MCP_API_KEY}`
		};
		if (isSlackWorkAppTool(tool) && isTrustedWorkAppMcpUrl(urlString, TRUSTED_WORK_APP_MCP_PATHS.slack, env.INKEEP_AGENTS_API_URL)) serverConfig.headers = {
			...serverConfig.headers,
			"x-inkeep-tool-id": tool.id,
			"x-inkeep-tenant-id": tool.tenantId,
			"x-inkeep-project-id": tool.projectId,
			Authorization: `Bearer ${env.SLACK_MCP_API_KEY}`
		};
		const client = new McpClient({
			name: tool.name,
			server: serverConfig
		});
		await client.connect();
		const serverTools = await client.tools();
		const serverInstructions = client.getInstructions() ?? void 0;
		await client.disconnect();
		const toolOverrides = tool.config.mcp.toolOverrides;
		return {
			tools: Object.entries(serverTools).map(([name, toolDef]) => {
				const schema = extractInputSchema(toolDef, name, toolOverrides);
				return {
					name,
					description: toolDef.description || "",
					inputSchema: schema
				};
			}),
			serverInstructions
		};
	} catch (error) {
		logger.error({
			toolId: tool.id,
			error
		}, "Tool discovery failed");
		throw error;
	}
};
/**
* Convert DB result to McpTool skeleton WITHOUT MCP discovery.
* This is a fast path that returns status='unknown' and empty availableTools.
* Use this for list views where you want instant page load.
*/
const dbResultToMcpToolSkeleton = (dbResult, relationshipId) => {
	const { headers, capabilities, credentialReferenceId, imageUrl, createdAt, ...rest } = dbResult;
	return {
		...rest,
		status: "unknown",
		availableTools: [],
		capabilities: capabilities || void 0,
		credentialReferenceId: credentialReferenceId || void 0,
		createdAt: toISODateString(createdAt),
		updatedAt: toISODateString(dbResult.updatedAt),
		lastError: dbResult.lastError || null,
		headers: headers || void 0,
		imageUrl: imageUrl || void 0,
		relationshipId
	};
};
const dbResultToMcpTool = async (dbResult, dbClient, credentialStoreRegistry, relationshipId, userId) => {
	const { headers, capabilities, credentialReferenceId, imageUrl, createdAt, ...rest } = dbResult;
	if (dbResult.config.type !== "mcp") return {
		...rest,
		status: "unknown",
		availableTools: [],
		capabilities: capabilities || void 0,
		credentialReferenceId: credentialReferenceId || void 0,
		createdAt: toISODateString(createdAt),
		updatedAt: toISODateString(dbResult.updatedAt),
		lastError: null,
		headers: headers || void 0,
		imageUrl: imageUrl || void 0,
		relationshipId
	};
	let availableTools = [];
	let status = "unknown";
	let lastErrorComputed;
	let expiresAt;
	let createdBy;
	let serverInstructions;
	const credentialReference = credentialReferenceId && dbResult.credentialScope !== "user" ? await getCredentialReference(dbClient)({
		scopes: {
			tenantId: dbResult.tenantId,
			projectId: dbResult.projectId
		},
		id: credentialReferenceId
	}) : userId && dbResult.credentialScope === "user" ? await getUserScopedCredentialReference(dbClient)({
		scopes: {
			tenantId: dbResult.tenantId,
			projectId: dbResult.projectId
		},
		toolId: dbResult.id,
		userId
	}) : void 0;
	if (credentialReference) {
		createdBy = credentialReference.createdBy || void 0;
		expiresAt = await getCredentialExpiresAt(credentialReference, credentialStoreRegistry);
	}
	const mcpServerUrl = dbResult.config.mcp.server.url;
	try {
		const discoveryResult = await discoverToolsFromServer(dbResult, credentialReference, credentialStoreRegistry, userId);
		availableTools = discoveryResult.tools;
		serverInstructions = discoveryResult.serverInstructions;
		status = "healthy";
		lastErrorComputed = null;
	} catch (error) {
		const errorMessage = error instanceof Error ? error.message : "Tool discovery failed";
		if (isTimeoutOrConnectionError(error)) {
			status = "unavailable";
			lastErrorComputed = `Connection failed - the MCP server may be slow or temporarily unreachable.${error instanceof McpError ? ` (MCP error ${error.code})` : ""} ${errorMessage}`;
		} else if (isAuthenticationError(error)) {
			status = "needs_auth";
			lastErrorComputed = `Authentication required - OAuth login needed. ${errorMessage}`;
		} else if (credentialReference) {
			logger.warn({
				toolId: dbResult.id,
				credentialId: credentialReference.id,
				errorCode: error instanceof McpError ? error.code : void 0,
				errorMessage
			}, "MCP server discovery failed with existing credential — treating as transient");
			status = "unavailable";
			lastErrorComputed = `Server temporarily unavailable. ${errorMessage}`;
		} else {
			const toolNeedsAuth = await detectAuthenticationRequired({
				serverUrl: mcpServerUrl,
				error: error instanceof Error ? error : void 0,
				logger
			});
			status = toolNeedsAuth ? "needs_auth" : "unhealthy";
			lastErrorComputed = toolNeedsAuth ? `Authentication required - OAuth login needed. ${errorMessage}` : errorMessage;
		}
	}
	if (dbResult.config.mcp.server.url.includes("composio.dev")) if (!!!credentialReference?.retrievalParams?.connectedAccountId) {
		status = "needs_auth";
		lastErrorComputed = "Third-party authentication required. Connect your account to pin a specific credential.";
	} else {
		const credentialScope = dbResult.credentialScope || "project";
		const authResult = await isThirdPartyMCPServerAuthenticated(dbResult.tenantId, dbResult.projectId, mcpServerUrl, credentialScope, userId);
		if (!authResult.authenticated && !authResult.error) {
			status = "needs_auth";
			lastErrorComputed = "Third-party authentication required. Try authenticating again.";
		} else if (authResult.error) {
			status = "unavailable";
			lastErrorComputed = "Could not verify third-party authentication status. The service may be temporarily unavailable.";
		}
	}
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const updatedCapabilities = {
		...capabilities,
		...serverInstructions !== void 0 && { serverInstructions }
	};
	try {
		await updateTool(dbClient)({
			scopes: {
				tenantId: dbResult.tenantId,
				projectId: dbResult.projectId
			},
			toolId: dbResult.id,
			data: {
				updatedAt: now,
				lastError: lastErrorComputed,
				capabilities: updatedCapabilities
			}
		});
	} catch (updateError) {
		if (isSerializationError(updateError)) logger.debug({ toolId: dbResult.id }, "Skipping tool metadata update due to serialization conflict (concurrent request)");
		else logger.warn({
			toolId: dbResult.id,
			error: updateError
		}, "Failed to update tool metadata - continuing with stale data");
	}
	return {
		...rest,
		status,
		availableTools,
		capabilities: Object.keys(updatedCapabilities).length > 0 ? updatedCapabilities : void 0,
		credentialReferenceId: credentialReferenceId || void 0,
		createdAt: toISODateString(createdAt),
		createdBy: createdBy || void 0,
		updatedAt: toISODateString(now),
		expiresAt,
		lastError: lastErrorComputed,
		headers: headers || void 0,
		imageUrl: imageUrl || void 0,
		relationshipId
	};
};
const getToolById = (db) => async (params) => {
	return await db.query.tools.findFirst({ where: and(projectScopedWhere(tools, params.scopes), eq(tools.id, params.toolId)) }) ?? null;
};
const getMcpToolById = (db) => async (params) => {
	const tool = await getToolById(db)({
		scopes: params.scopes,
		toolId: params.toolId
	});
	if (!tool) return null;
	return await dbResultToMcpTool(tool, db, params.credentialStoreRegistry, params.relationshipId, params.userId);
};
const listTools = (db) => async (params) => {
	const page = params.pagination?.page || 1;
	const limit = Math.min(params.pagination?.limit || 10, 100);
	const offset = (page - 1) * limit;
	const whereClause = projectScopedWhere(tools, params.scopes);
	const [toolsDbResults, totalResult] = await Promise.all([db.select().from(tools).where(whereClause).limit(limit).offset(offset).orderBy(desc(tools.createdAt)), db.select({ count: count() }).from(tools).where(whereClause)]);
	const total = totalResult[0]?.count || 0;
	return {
		data: toolsDbResults,
		pagination: {
			page,
			limit,
			total,
			pages: Math.ceil(total / limit)
		}
	};
};
const createTool = (db) => async (params) => {
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const [created] = await db.insert(tools).values({
		...params,
		createdAt: now,
		updatedAt: now
	}).returning();
	return created;
};
const updateTool = (db) => async (params) => {
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const [updated] = await db.update(tools).set({
		...params.data,
		updatedAt: now
	}).where(and(projectScopedWhere(tools, params.scopes), eq(tools.id, params.toolId))).returning();
	return updated ?? null;
};
const deleteTool = (db) => async (params) => {
	const [deleted] = await db.delete(tools).where(and(projectScopedWhere(tools, params.scopes), eq(tools.id, params.toolId))).returning();
	if (!deleted) return false;
	if (deleted.isWorkApp && deleted.config.mcp.server.url.includes("/github/mcp") || isSlackWorkAppTool(deleted)) try {
		if (await getActiveBranch(db)() === `${params.scopes.tenantId}_${params.scopes.projectId}_main`) await cascadeDeleteByTool(createAgentsRunDatabaseClient())({
			toolId: params.toolId,
			tenantId: params.scopes.tenantId,
			projectId: params.scopes.projectId
		});
	} catch (error) {
		logger.debug({
			error,
			toolId: params.toolId
		}, "Skipping cascade delete - active_branch() not available");
	}
	return true;
};
const addToolToAgent = (db) => async (params) => {
	const id = deriveRelationId(params.scopes.tenantId, params.scopes.projectId, params.scopes.agentId, params.subAgentId, params.toolId);
	const now = (/* @__PURE__ */ new Date()).toISOString();
	const [created] = await db.insert(subAgentToolRelations).values({
		id,
		tenantId: params.scopes.tenantId,
		projectId: params.scopes.projectId,
		agentId: params.scopes.agentId,
		subAgentId: params.subAgentId,
		toolId: params.toolId,
		selectedTools: params.selectedTools,
		headers: params.headers,
		toolPolicies: params.toolPolicies,
		createdAt: now,
		updatedAt: now
	}).returning();
	return created;
};
const removeToolFromAgent = (db) => async (params) => {
	const [deleted] = await db.delete(subAgentToolRelations).where(and(agentScopedWhere(subAgentToolRelations, params.scopes), eq(subAgentToolRelations.subAgentId, params.subAgentId), eq(subAgentToolRelations.toolId, params.toolId))).returning();
	return deleted;
};
/**
* Upsert agent-tool relation (create if it doesn't exist, update if it does)
*/
const upsertSubAgentToolRelation = (db) => async (params) => {
	if (params.relationId) return await updateAgentToolRelation(db)({
		scopes: params.scopes,
		relationId: params.relationId,
		data: {
			subAgentId: params.subAgentId,
			toolId: params.toolId,
			selectedTools: params.selectedTools,
			headers: params.headers,
			toolPolicies: params.toolPolicies
		}
	});
	return await addToolToAgent(db)(params);
};
/**
* Upsert a tool (create if it doesn't exist, update if it does)
*/
const upsertTool = (db) => async (params) => {
	const scopes = {
		tenantId: params.data.tenantId,
		projectId: params.data.projectId
	};
	if (await getToolById(db)({
		scopes,
		toolId: params.data.id
	})) return await updateTool(db)({
		scopes,
		toolId: params.data.id,
		data: {
			name: params.data.name,
			config: params.data.config,
			credentialReferenceId: params.data.credentialReferenceId,
			imageUrl: params.data.imageUrl,
			headers: params.data.headers
		}
	});
	return await createTool(db)(params.data);
};

//#endregion
export { addToolToAgent, createTool, dbResultToMcpTool, dbResultToMcpToolSkeleton, deleteTool, getMcpToolById, getToolById, listTools, removeToolFromAgent, updateTool, upsertSubAgentToolRelation, upsertTool };