import { getLogger as getLogger$1 } from "../../../logger.js";
import manageDbClient_default from "../../../data/db/manageDbClient.js";
import runDbClient_default from "../../../data/db/runDbClient.js";
import { requireProjectPermission } from "../../../middleware/projectAccess.js";
import { onTriggerCreated, onTriggerDeleted, onTriggerUpdated } from "../../run/services/ScheduledTriggerService.js";
import { requirePermission } from "../../../middleware/requirePermission.js";
import { assertCanMutateTrigger, isScheduledTriggerChanged, validateRunAsUserId } from "./scheduledTriggers.js";
import { OpenAPIHono, z } from "@hono/zod-openapi";
import { ErrorResponseSchema, FullProjectDefinitionSchema, FullProjectSelectResponse, FullProjectSelectWithRelationIdsResponse, OrgRoles, TenantParamsSchema, TenantProjectParamsSchema, cascadeDeleteByProject, checkoutBranch, commonGetErrorResponses, createApiError, createFullProjectServerSide, createProjectMetadataAndBranch, deleteFullProject, deleteProjectWithBranch, doltCheckout, getFullProject, getFullProjectWithRelationIds, getProjectMainBranchName, getProjectMetadata, listScheduledTriggers, removeProjectFromSpiceDb, syncProjectToSpiceDb, throwIfUniqueConstraintError, updateFullProjectServerSide } from "@inkeep/agents-core";
import { createProtectedRoute, registerAuthzMeta } from "@inkeep/agents-core/middleware";

//#region src/domains/manage/routes/projectFull.ts
const logger = getLogger$1("projectFull");
const app = new OpenAPIHono();
const requireProjectUpsertPermission = async (c, next) => {
	const tenantId = c.get("tenantId");
	const projectId = c.req.param("projectId");
	if (!tenantId || !projectId) throw createApiError({
		code: "bad_request",
		message: "Missing tenantId or projectId"
	});
	const exists = await getProjectMetadata(runDbClient_default)({
		tenantId,
		projectId
	});
	c.set("isProjectCreate", !exists);
	return exists ? requireProjectPermission("edit")(c, next) : requirePermission({ project: ["create"] })(c, next);
};
registerAuthzMeta(requireProjectUpsertPermission, { description: "Dynamic: requires organization admin for new projects, or project edit permission for existing projects." });
app.openapi(createProtectedRoute({
	method: "post",
	path: "/project-full",
	summary: "Create Full Project",
	operationId: "create-full-project",
	tags: ["Projects"],
	description: "Create a complete project with all Agents, Sub Agents, tools, and relationships from JSON definition",
	permission: requirePermission({ project: ["create"] }),
	request: {
		params: TenantParamsSchema,
		body: { content: { "application/json": { schema: FullProjectDefinitionSchema } } }
	},
	responses: {
		201: {
			description: "Full project created successfully",
			content: { "application/json": { schema: FullProjectSelectResponse } }
		},
		409: {
			description: "Project already exists",
			content: { "application/json": { schema: ErrorResponseSchema } }
		},
		...commonGetErrorResponses
	}
}), async (c) => {
	const configDb = c.get("db");
	const userId = c.get("userId");
	const { tenantId } = c.req.valid("param");
	const projectData = c.req.valid("json");
	const validatedProjectData = FullProjectDefinitionSchema.parse(projectData);
	try {
		const createdProject = await runDbClient_default.transaction(async (runTx) => {
			return await configDb.transaction(async (configTx) => {
				await createProjectMetadataAndBranch(runTx, configTx)({
					tenantId,
					projectId: validatedProjectData.id,
					createdBy: userId
				});
				logger.info({
					tenantId,
					projectId: validatedProjectData.id
				}, "Created project with branch, now populating config");
				const projectMainBranch = getProjectMainBranchName(tenantId, validatedProjectData.id);
				await checkoutBranch(configTx)({
					branchName: projectMainBranch,
					autoCommitPending: true
				});
				const newResolvedRef = {
					type: "branch",
					name: projectMainBranch,
					hash: ""
				};
				c.set("resolvedRef", newResolvedRef);
				logger.debug({ projectMainBranch }, "Checked out project branch for config writes");
				const project = await createFullProjectServerSide(configTx)({
					scopes: {
						tenantId,
						projectId: validatedProjectData.id
					},
					projectData: validatedProjectData
				});
				if (userId) await syncProjectToSpiceDb({
					tenantId,
					projectId: validatedProjectData.id,
					creatorUserId: userId
				});
				else logger.warn({
					tenantId,
					projectId: validatedProjectData.id
				}, "Skipping SpiceDB sync — no userId available");
				return project;
			});
		});
		return c.json({ data: createdProject }, 201);
	} catch (error) {
		logger.error({ error }, "Error creating project");
		throwIfUniqueConstraintError(error, `Project with ID '${projectData.id}' already exists`);
		const isGrpcError = error?.metadata !== void 0 && typeof error?.code === "number";
		if (error?.message?.includes("SpiceDB") || isGrpcError) {
			logger.error({
				error,
				tenantId,
				projectId: validatedProjectData.id,
				userId
			}, "Failed to sync project to SpiceDB — database transactions rolled back");
			throw createApiError({
				code: "internal_server_error",
				message: "Failed to set up project authorization. No changes were made to the database."
			});
		}
		throw error;
	}
});
app.openapi(createProtectedRoute({
	method: "get",
	path: "/project-full/{projectId}",
	summary: "Get Full Project",
	operationId: "get-full-project",
	tags: ["Projects"],
	description: "Retrieve a complete project definition with all Agents, Sub Agents, tools, and relationships",
	permission: requireProjectPermission("view"),
	request: { params: TenantProjectParamsSchema },
	responses: {
		200: {
			description: "Full project found",
			content: { "application/json": { schema: FullProjectSelectResponse } }
		},
		...commonGetErrorResponses
	}
}), async (c) => {
	const { tenantId, projectId } = c.req.valid("param");
	const db = c.get("db");
	try {
		const project = await getFullProject(db)({ scopes: {
			tenantId,
			projectId
		} });
		if (!project) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		return c.json({ data: project });
	} catch (error) {
		if (error instanceof Error && error.message.includes("not found")) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		throw createApiError({
			code: "internal_server_error",
			message: error instanceof Error ? error.message : "Failed to retrieve project"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "get",
	path: "/project-full/{projectId}/with-relation-ids",
	summary: "Get Full Project with Relation IDs",
	operationId: "get-full-project-with-relation-ids",
	tags: ["Projects"],
	description: "Retrieve a complete project definition with all Agents, Sub Agents, tools, and relationships",
	permission: requireProjectPermission("view"),
	request: { params: TenantProjectParamsSchema },
	responses: {
		200: {
			description: "Full project found",
			content: { "application/json": { schema: FullProjectSelectWithRelationIdsResponse } }
		},
		...commonGetErrorResponses
	}
}), async (c) => {
	const { tenantId, projectId } = c.req.valid("param");
	const db = c.get("db");
	try {
		const project = await getFullProjectWithRelationIds(db)({ scopes: {
			tenantId,
			projectId
		} });
		if (!project) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		return c.json({ data: project });
	} catch (error) {
		if (error instanceof Error && error.message.includes("not found")) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		throw createApiError({
			code: "internal_server_error",
			message: error instanceof Error ? error.message : "Failed to retrieve project"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "put",
	path: "/project-full/{projectId}",
	summary: "Update Full Project",
	operationId: "update-full-project",
	tags: ["Projects"],
	description: "Update or create a complete project with all Agents, Sub Agents, tools, and relationships from JSON definition",
	permission: requireProjectUpsertPermission,
	request: {
		params: TenantProjectParamsSchema,
		body: { content: { "application/json": { schema: FullProjectDefinitionSchema } } }
	},
	responses: {
		200: {
			description: "Full project updated successfully",
			content: { "application/json": { schema: FullProjectSelectResponse } }
		},
		201: {
			description: "Full project created successfully",
			content: { "application/json": { schema: FullProjectSelectResponse } }
		},
		...commonGetErrorResponses
	}
}), async (c) => {
	const { tenantId, projectId } = c.req.valid("param");
	const projectData = c.req.valid("json");
	const configDb = c.get("db");
	const userId = c.get("userId");
	const isCreate = c.get("isProjectCreate") ?? false;
	try {
		const validatedProjectData = FullProjectDefinitionSchema.parse(projectData);
		if (projectId !== validatedProjectData.id) throw createApiError({
			code: "bad_request",
			message: `Project ID mismatch: expected ${projectId}, got ${validatedProjectData.id}`
		});
		const existingTriggersByAgent = /* @__PURE__ */ new Map();
		if (!isCreate) {
			const agents = Object.keys(validatedProjectData.agents || {});
			for (const agentId of agents) {
				const existingTriggers = await listScheduledTriggers(configDb)({ scopes: {
					tenantId,
					projectId,
					agentId
				} });
				existingTriggersByAgent.set(agentId, existingTriggers);
			}
		}
		const callerId = userId ?? "";
		const tenantRole = c.get("tenantRole") || OrgRoles.MEMBER;
		for (const [agentId, agentData] of Object.entries(validatedProjectData.agents)) {
			if (!agentData.scheduledTriggers) continue;
			const existingTriggers = existingTriggersByAgent.get(agentId) || [];
			const existingById = new Map(existingTriggers.map((t) => [t.id, t]));
			for (const [triggerId, triggerData] of Object.entries(agentData.scheduledTriggers)) {
				const existing = existingById.get(triggerId);
				if (existing) {
					if (!isScheduledTriggerChanged(triggerData, existing)) continue;
					assertCanMutateTrigger({
						trigger: existing,
						callerId,
						tenantRole
					});
					if (triggerData.runAsUserId !== existing.runAsUserId && triggerData.runAsUserId) await validateRunAsUserId({
						runAsUserId: triggerData.runAsUserId,
						callerId,
						tenantId,
						projectId,
						tenantRole
					});
				} else {
					if (triggerData.runAsUserId) await validateRunAsUserId({
						runAsUserId: triggerData.runAsUserId,
						callerId,
						tenantId,
						projectId,
						tenantRole
					});
					triggerData.createdBy = callerId;
				}
			}
		}
		const updatedProject = isCreate ? await runDbClient_default.transaction(async (runTx) => {
			return await configDb.transaction(async (configTx) => {
				await createProjectMetadataAndBranch(runTx, configTx)({
					tenantId,
					projectId,
					createdBy: userId
				});
				logger.info({
					tenantId,
					projectId
				}, "Created project with branch for PUT (upsert)");
				const projectMainBranch = getProjectMainBranchName(tenantId, projectId);
				await checkoutBranch(configTx)({
					branchName: projectMainBranch,
					autoCommitPending: true
				});
				const project = await createFullProjectServerSide(configTx)({
					scopes: {
						tenantId,
						projectId
					},
					projectData: validatedProjectData
				});
				if (userId) await syncProjectToSpiceDb({
					tenantId,
					projectId,
					creatorUserId: userId
				});
				else logger.warn({
					tenantId,
					projectId
				}, "Skipping SpiceDB sync — no userId available");
				return project;
			});
		}) : await updateFullProjectServerSide(configDb)({
			scopes: {
				tenantId,
				projectId
			},
			projectData: validatedProjectData
		});
		try {
			const agents = Object.keys(validatedProjectData.agents || {});
			logger.info({
				tenantId,
				projectId,
				agentIds: agents,
				agentCount: agents.length
			}, "Starting scheduled trigger workflow reconciliation");
			await Promise.all(agents.map(async (agentId) => {
				const existingTriggersForAgent = existingTriggersByAgent.get(agentId) || [];
				const newTriggersForAgent = await listScheduledTriggers(configDb)({ scopes: {
					tenantId,
					projectId,
					agentId
				} });
				logger.info({
					tenantId,
					projectId,
					agentId,
					existingCount: existingTriggersForAgent.length,
					newCount: newTriggersForAgent.length
				}, "Reconciling scheduled triggers for agent");
				const existingTriggerMap = new Map(existingTriggersForAgent.map((t) => [t.id, t]));
				const newTriggerMap = new Map(newTriggersForAgent.map((t) => [t.id, t]));
				const workflowOperations = [];
				for (const trigger of newTriggersForAgent) {
					const existing = existingTriggerMap.get(trigger.id);
					if (!existing) workflowOperations.push(onTriggerCreated(trigger).then(() => logger.info({
						tenantId,
						projectId,
						agentId,
						scheduledTriggerId: trigger.id
					}, "Started workflow for new scheduled trigger")).catch((err) => logger.error({
						err,
						tenantId,
						projectId,
						agentId,
						scheduledTriggerId: trigger.id
					}, "Failed to start workflow for new scheduled trigger")));
					else {
						const scheduleChanged = existing.cronExpression !== trigger.cronExpression || String(existing.runAt) !== String(trigger.runAt);
						const previousEnabled = existing.enabled;
						if (scheduleChanged || previousEnabled !== trigger.enabled) workflowOperations.push(onTriggerUpdated({
							trigger,
							previousEnabled,
							scheduleChanged
						}).then(() => logger.info({
							tenantId,
							projectId,
							agentId,
							scheduledTriggerId: trigger.id
						}, "Updated workflow for scheduled trigger")).catch((err) => logger.error({
							err,
							tenantId,
							projectId,
							agentId,
							scheduledTriggerId: trigger.id
						}, "Failed to update workflow for scheduled trigger")));
					}
				}
				for (const existing of existingTriggersForAgent) if (!newTriggerMap.has(existing.id)) workflowOperations.push(onTriggerDeleted(existing).then(() => logger.info({
					tenantId,
					projectId,
					agentId,
					scheduledTriggerId: existing.id
				}, "Stopped workflow for deleted scheduled trigger")).catch((err) => logger.error({
					err,
					tenantId,
					projectId,
					agentId,
					scheduledTriggerId: existing.id
				}, "Failed to stop workflow for deleted scheduled trigger")));
				await Promise.allSettled(workflowOperations);
			}));
			logger.info({
				tenantId,
				projectId,
				agentCount: agents.length
			}, "Completed scheduled trigger workflow reconciliation");
		} catch (err) {
			logger.error({
				err,
				tenantId,
				projectId
			}, "Failed to reconcile scheduled trigger workflows after project update");
		}
		return c.json({ data: updatedProject }, isCreate ? 201 : 200);
	} catch (error) {
		if (error instanceof z.ZodError) throw createApiError({
			code: "bad_request",
			message: "Invalid project definition"
		});
		if (error instanceof Error && error.message.includes("ID mismatch")) throw createApiError({
			code: "bad_request",
			message: error.message
		});
		if (isCreate) {
			const isGrpcError = error?.metadata !== void 0 && typeof error?.code === "number";
			if (error?.message?.includes("SpiceDB") || isGrpcError) {
				logger.error({
					error,
					tenantId,
					projectId,
					userId
				}, "Failed to sync project to SpiceDB — database transactions rolled back");
				throw createApiError({
					code: "internal_server_error",
					message: "Failed to set up project authorization. No changes were made to the database."
				});
			}
		}
		throw createApiError({
			code: "internal_server_error",
			message: error instanceof Error ? error.message : "Failed to update project"
		});
	}
});
app.openapi(createProtectedRoute({
	method: "delete",
	path: "/project-full/{projectId}",
	summary: "Delete Full Project",
	operationId: "delete-full-project",
	tags: ["Projects"],
	description: "Delete a complete project and cascade to all related entities (Agents, Sub Agents, tools, relationships)",
	permission: requirePermission({ project: ["delete"] }),
	request: { params: TenantProjectParamsSchema },
	responses: {
		204: { description: "Project deleted successfully" },
		...commonGetErrorResponses
	}
}), async (c) => {
	const { tenantId, projectId } = c.req.valid("param");
	const configDb = c.get("db");
	const resolvedRef = c.get("resolvedRef");
	const expectedMainBranch = `${tenantId}_${projectId}_main`;
	if (resolvedRef?.name !== expectedMainBranch) throw createApiError({
		code: "bad_request",
		message: "Project deletion must be performed from the main branch"
	});
	try {
		await cascadeDeleteByProject(runDbClient_default)({
			scopes: {
				tenantId,
				projectId
			},
			fullBranchName: resolvedRef.name
		});
		await deleteFullProject(configDb)({ scopes: {
			tenantId,
			projectId
		} });
		await doltCheckout(configDb)({ branch: "main" });
		if (!await deleteProjectWithBranch(runDbClient_default, manageDbClient_default)({
			tenantId,
			projectId
		})) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		try {
			await removeProjectFromSpiceDb({
				tenantId,
				projectId
			});
			logger.info({
				tenantId,
				projectId
			}, "Removed project from SpiceDB");
		} catch (error) {
			logger.warn({
				error,
				tenantId,
				projectId
			}, "Failed to remove project from SpiceDB - orphaned auth relationships may remain");
		}
		return c.body(null, 204);
	} catch (error) {
		if (error instanceof Error && error.message.includes("not found")) throw createApiError({
			code: "not_found",
			message: "Project not found"
		});
		throw createApiError({
			code: "internal_server_error",
			message: error instanceof Error ? error.message : "Failed to delete project"
		});
	}
});
var projectFull_default = app;

//#endregion
export { projectFull_default as default };