import { env } from "../../env.js";
import manageDbClient_default from "../../data/db/manageDbClient.js";
import manageDbPool_default from "../../data/db/manageDbPool.js";
import runDbClient_default from "../../data/db/runDbClient.js";
import { getLogger as getLogger$1 } from "../../logger.js";
import { getOAuthIssuer, getOAuthJwks } from "../../utils/oauthJwks.js";
import { OpenAPIHono, z } from "@hono/zod-openapi";
import { SUPPORT_COPILOT_PLATFORMS, canAppReadCredential, canUseProjectStrict, getAppById, getCredentialReference, getCredentialStoreLookupKeyFromRetrievalParams, getProjectScopedRef, resolveRef, withRef } from "@inkeep/agents-core";
import { timingSafeEqual } from "node:crypto";
import { createProtectedRoute, noAuth } from "@inkeep/agents-core/middleware";
import { jwtVerify } from "jose";

//#region src/domains/credential-gateway/routes.ts
const logger = getLogger$1("credential-gateway");
const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
const TokenExchangeBodySchema = z.object({
	grant_type: z.literal(TOKEN_EXCHANGE_GRANT_TYPE),
	subject_token: z.string().min(1),
	subject_token_type: z.literal(ACCESS_TOKEN_TYPE),
	audience: z.string().min(1),
	inkeep_app_id: z.string().startsWith("app_"),
	resource: z.string().optional()
});
const TokenExchangeResponseSchema = z.object({
	access_token: z.string(),
	token_type: z.literal("Bearer"),
	expires_in: z.number(),
	issued_token_type: z.literal(ACCESS_TOKEN_TYPE)
});
const ErrorResponseSchema$1 = z.object({
	error: z.string(),
	error_description: z.string()
});
function rfc8693Error(status, error, errorDescription) {
	return {
		status,
		body: {
			error,
			error_description: errorDescription
		}
	};
}
function safeTimingSafeEqual(a, b) {
	const bufA = Buffer.from(a);
	const bufB = Buffer.from(b);
	if (bufA.length !== bufB.length) return false;
	return timingSafeEqual(bufA, bufB);
}
const app = new OpenAPIHono();
app.openapi(createProtectedRoute({
	method: "post",
	path: "/token",
	permission: noAuth(),
	security: [],
	summary: "Token Exchange (RFC 8693)",
	description: "Exchange a user JWT for a third-party credential via RFC 8693 token exchange. Server-to-server only.",
	operationId: "exchange-credential-gateway-token",
	tags: ["Credential Gateway"],
	request: { body: {
		required: true,
		content: { "application/x-www-form-urlencoded": { schema: TokenExchangeBodySchema } }
	} },
	responses: {
		200: {
			description: "Token exchange successful",
			content: { "application/json": { schema: TokenExchangeResponseSchema } }
		},
		400: {
			description: "Bad request (invalid_request, invalid_target)",
			content: { "application/json": { schema: ErrorResponseSchema$1 } }
		},
		401: {
			description: "Unauthorized (invalid_client, invalid_grant)",
			content: { "application/json": { schema: ErrorResponseSchema$1 } }
		},
		403: {
			description: "Forbidden (access_denied)",
			content: { "application/json": { schema: ErrorResponseSchema$1 } }
		},
		502: {
			description: "Upstream credential store error",
			content: { "application/json": { schema: ErrorResponseSchema$1 } }
		}
	}
}), (async (c) => {
	const body = c.req.valid("form");
	const authHeader = c.req.header("Authorization");
	if (!authHeader?.startsWith("Basic ")) {
		logger.info({
			outcome: "invalid_client",
			reason: "missing_header"
		}, "Token exchange denied");
		const err = rfc8693Error(401, "invalid_client", "Missing or malformed Authorization header");
		return c.json(err.body, err.status);
	}
	const gatewayClientId = env.COPILOT_GATEWAY_CLIENT_ID;
	const gatewayClientSecret = env.COPILOT_GATEWAY_CLIENT_SECRET;
	if (!gatewayClientId || !gatewayClientSecret) {
		logger.warn({}, "Credential gateway not configured: missing COPILOT_GATEWAY_CLIENT_ID or COPILOT_GATEWAY_CLIENT_SECRET");
		const err = rfc8693Error(401, "invalid_client", "Credential gateway not configured");
		return c.json(err.body, err.status);
	}
	let clientId;
	let clientSecret;
	try {
		const decoded = atob(authHeader.slice(6));
		const colonIdx = decoded.indexOf(":");
		if (colonIdx === -1) throw new Error("No colon");
		clientId = decoded.slice(0, colonIdx);
		clientSecret = decoded.slice(colonIdx + 1);
	} catch {
		logger.info({
			outcome: "invalid_client",
			reason: "malformed_basic"
		}, "Token exchange denied");
		const err = rfc8693Error(401, "invalid_client", "Malformed Basic credentials");
		return c.json(err.body, err.status);
	}
	if (!safeTimingSafeEqual(clientId, gatewayClientId) || !safeTimingSafeEqual(clientSecret, gatewayClientSecret)) {
		logger.info({ outcome: "invalid_client" }, "Token exchange denied");
		const err = rfc8693Error(401, "invalid_client", "Invalid client credentials");
		return c.json(err.body, err.status);
	}
	let sub;
	let tenantId;
	try {
		const payload = (await jwtVerify(body.subject_token, getOAuthJwks(), { issuer: getOAuthIssuer() })).payload;
		const jwtSub = payload.sub;
		const azp = payload.azp;
		const jwtTenantId = payload["https://inkeep.com/tenantId"];
		if (!jwtSub || !jwtTenantId) throw new Error("Missing required claims");
		if (azp !== env.COPILOT_OAUTH_CLIENT_ID) throw new Error("Invalid authorized party");
		sub = jwtSub;
		tenantId = jwtTenantId;
	} catch {
		logger.info({ outcome: "bad_jwt" }, "Token exchange denied");
		const err = rfc8693Error(401, "invalid_grant", "Invalid or expired subject token");
		return c.json(err.body, err.status);
	}
	const appRecord = await getAppById(runDbClient_default)(body.inkeep_app_id);
	if (!appRecord || appRecord.tenantId !== tenantId || !appRecord.enabled || appRecord.type !== "support_copilot" || !appRecord.projectId) {
		logger.info({
			outcome: "access_denied",
			userSub: sub,
			appId: body.inkeep_app_id
		}, "Token exchange denied");
		const err = rfc8693Error(403, "access_denied", "Access denied");
		return c.json(err.body, err.status);
	}
	const { tenantId: appTenantId, projectId: appProjectId } = appRecord;
	if (!await canUseProjectStrict({
		userId: sub,
		tenantId: appTenantId,
		projectId: appProjectId
	})) {
		logger.info({
			outcome: "project_denied",
			userSub: sub,
			appId: body.inkeep_app_id
		}, "Token exchange denied");
		const err = rfc8693Error(403, "access_denied", "Access denied");
		return c.json(err.body, err.status);
	}
	const supportCopilotConfig = appRecord.config?.type === "support_copilot" ? appRecord.config.supportCopilot : void 0;
	if (!supportCopilotConfig?.platform || !supportCopilotConfig?.credentialReferenceId) {
		logger.info({
			outcome: "app_not_configured",
			userSub: sub,
			appId: body.inkeep_app_id
		}, "Token exchange denied");
		const err = rfc8693Error(403, "access_denied", "Access denied");
		return c.json(err.body, err.status);
	}
	if (body.audience !== supportCopilotConfig.platform) {
		logger.info({
			outcome: "audience_mismatch",
			userSub: sub,
			appId: body.inkeep_app_id,
			audience: body.audience,
			platform: supportCopilotConfig.platform
		}, "Token exchange denied");
		const err = rfc8693Error(400, "invalid_target", "Audience does not match app configuration");
		return c.json(err.body, err.status);
	}
	const credentialReferenceId = supportCopilotConfig.credentialReferenceId;
	if (!await canAppReadCredential({
		tenantId: appTenantId,
		projectId: appProjectId,
		credentialReferenceId,
		appId: appRecord.id
	})) {
		logger.info({
			outcome: "credential_denied",
			userSub: sub,
			appId: body.inkeep_app_id,
			credentialReferenceId
		}, "Token exchange denied");
		const err = rfc8693Error(403, "access_denied", "Access denied");
		return c.json(err.body, err.status);
	}
	const projectRef = getProjectScopedRef(appTenantId, appProjectId, "main");
	const resolvedRef = await resolveRef(manageDbClient_default)(projectRef);
	if (!resolvedRef) {
		logger.info({
			outcome: "project_branch_missing",
			userSub: sub,
			appId: body.inkeep_app_id
		}, "Token exchange denied");
		const err = rfc8693Error(502, "server_error", "Credential unavailable");
		return c.json(err.body, err.status);
	}
	const credRef = await withRef(manageDbPool_default, resolvedRef, (db) => getCredentialReference(db)({
		scopes: {
			tenantId: appTenantId,
			projectId: appProjectId
		},
		id: credentialReferenceId
	}));
	if (!credRef) {
		logger.warn({
			outcome: "credential_missing",
			credentialReferenceId
		}, "Configured credential not found");
		const err = rfc8693Error(502, "server_error", "Credential unavailable");
		return c.json(err.body, err.status);
	}
	const store = c.get("credentialStores").get(credRef.credentialStoreId);
	if (!store || !credRef.retrievalParams) {
		logger.warn({
			outcome: "nango_error",
			credentialReferenceId: credRef.id
		}, "Credential store not found or missing retrieval params");
		const err = rfc8693Error(502, "server_error", "Credential retrieval failed");
		return c.json(err.body, err.status);
	}
	const lookupKey = getCredentialStoreLookupKeyFromRetrievalParams({
		retrievalParams: credRef.retrievalParams,
		credentialStoreType: store.type
	});
	if (!lookupKey) {
		logger.warn({
			outcome: "nango_error",
			credentialReferenceId: credRef.id
		}, "Could not build credential lookup key");
		const err = rfc8693Error(502, "server_error", "Credential retrieval failed");
		return c.json(err.body, err.status);
	}
	let credentialJson;
	try {
		credentialJson = await store.get(lookupKey);
	} catch (error) {
		logger.warn({
			outcome: "nango_error",
			credentialReferenceId: credRef.id,
			error
		}, "Credential store fetch failed");
		const err = rfc8693Error(502, "server_error", "Credential retrieval failed");
		return c.json(err.body, err.status);
	}
	if (!credentialJson) {
		logger.warn({
			outcome: "nango_error",
			credentialReferenceId: credRef.id
		}, "Credential store returned null");
		const err = rfc8693Error(502, "server_error", "Credential retrieval failed");
		return c.json(err.body, err.status);
	}
	let accessToken;
	let expiresIn;
	try {
		const parsed = JSON.parse(credentialJson);
		accessToken = parsed.access_token || parsed.token;
		if (!accessToken) throw new Error("No access_token in credential data");
		if (parsed.expires_in) expiresIn = parsed.expires_in;
		else if (parsed.expiresAt) expiresIn = Math.max(0, Math.floor((new Date(parsed.expiresAt).getTime() - Date.now()) / 1e3));
		else expiresIn = 3600;
	} catch (error) {
		logger.warn({
			outcome: "nango_error",
			credentialReferenceId: credRef.id,
			error
		}, "Failed to parse credential data");
		const err = rfc8693Error(502, "server_error", "Credential retrieval failed");
		return c.json(err.body, err.status);
	}
	logger.info({
		outcome: "ok",
		clientId: gatewayClientId,
		userSub: sub,
		appId: body.inkeep_app_id,
		credentialReferenceId: credRef.id
	}, "Token exchange successful");
	return c.json({
		access_token: accessToken,
		token_type: "Bearer",
		expires_in: expiresIn,
		issued_token_type: "urn:ietf:params:oauth:token-type:access_token"
	}, 200);
}));
const PageMatcherSchema = z.object({
	pageType: z.string(),
	hostGlob: z.string(),
	pathPattern: z.string()
});
const PlatformEntrySchema = z.object({
	slug: z.string(),
	label: z.string(),
	credentialRequired: z.boolean(),
	pageMatchers: z.array(PageMatcherSchema)
});
const PlatformCatalogResponseSchema = z.object({ platforms: z.array(PlatformEntrySchema) });
app.openapi(createProtectedRoute({
	method: "get",
	path: "/.well-known/platforms",
	summary: "Support Copilot platform catalog",
	description: "Returns the list of supported platforms along with URL matchers. Consumed by browser extensions to detect the current tab platform and ticket ID. Public — safe to cache.",
	operationId: "list-credential-gateway-platforms",
	tags: ["Credential Gateway"],
	permission: noAuth(),
	security: [],
	responses: { 200: {
		description: "Platform catalog",
		content: { "application/json": { schema: PlatformCatalogResponseSchema } }
	} }
}), ((c) => {
	c.header("Cache-Control", "public, max-age=3600");
	return c.json({ platforms: SUPPORT_COPILOT_PLATFORMS }, 200);
}));

//#endregion
export { app as credentialGatewayRoutes };