import { Router } from "express";
import { createProjectSchema, createProjectWorkspaceSchema, findWorkspaceCommandDefinition, isUuidLike, matchWorkspaceRuntimeServiceToCommand, updateProjectSchema, updateProjectWorkspaceSchema, workspaceRuntimeControlTargetSchema, } from "@paperclipai/shared";
import { trackProjectCreated } from "@paperclipai/shared/telemetry";
import { validate } from "../middleware/validate.js";
import { accessService, projectService, logActivity, workspaceOperationService } from "../services/index.js";
import { conflict, forbidden } from "../errors.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js";
import { buildWorkspaceRuntimeDesiredStatePatch, listConfiguredRuntimeServiceEntries, runWorkspaceJobForControl, startRuntimeServicesForWorkspaceControl, stopRuntimeServicesForProjectWorkspace, } from "../services/workspace-runtime.js";
import { assertNoAgentHostWorkspaceCommandMutation, collectProjectExecutionWorkspaceCommandPaths, collectProjectWorkspaceCommandPaths, } from "./workspace-command-authz.js";
import { assertCanManageProjectWorkspaceRuntimeServices } from "./workspace-runtime-service-authz.js";
import { getTelemetryClient } from "../telemetry.js";
import { appendWithCap } from "../adapters/utils.js";
import { assertEnvironmentSelectionForCompany } from "./environment-selection.js";
import { environmentService } from "../services/environments.js";
import { secretService } from "../services/secrets.js";
const WORKSPACE_CONTROL_OUTPUT_MAX_CHARS = 256 * 1024;
const SHARED_WORKSPACE_STOP_AND_RESTART_ACTIONS = new Set(["stop", "restart"]);
export function projectRoutes(db) {
    const router = Router();
    const svc = projectService(db);
    const access = accessService(db);
    const secretsSvc = secretService(db);
    const workspaceOperations = workspaceOperationService(db);
    const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true";
    const environmentsSvc = environmentService(db);
    async function assertProjectEnvironmentSelection(companyId, environmentId) {
        if (environmentId === undefined || environmentId === null)
            return;
        await assertEnvironmentSelectionForCompany(environmentsSvc, companyId, environmentId, {
            allowedDrivers: ["local", "ssh", "sandbox"],
        });
    }
    function readProjectPolicyEnvironmentId(policy) {
        if (!policy || typeof policy !== "object" || !("environmentId" in policy)) {
            return undefined;
        }
        const environmentId = policy.environmentId;
        return typeof environmentId === "string" || environmentId === null ? environmentId : undefined;
    }
    async function resolveCompanyIdForProjectReference(req) {
        const companyIdQuery = req.query.companyId;
        const requestedCompanyId = typeof companyIdQuery === "string" && companyIdQuery.trim().length > 0
            ? companyIdQuery.trim()
            : null;
        if (requestedCompanyId) {
            assertCompanyAccess(req, requestedCompanyId);
            return requestedCompanyId;
        }
        if (req.actor.type === "agent" && req.actor.companyId) {
            return req.actor.companyId;
        }
        return null;
    }
    async function normalizeProjectReference(req, rawId) {
        if (isUuidLike(rawId))
            return rawId;
        const companyId = await resolveCompanyIdForProjectReference(req);
        if (!companyId)
            return rawId;
        const resolved = await svc.resolveByReference(companyId, rawId);
        if (resolved.ambiguous) {
            throw conflict("Project shortname is ambiguous in this company. Use the project ID.");
        }
        return resolved.project?.id ?? rawId;
    }
    async function assertProjectReadAllowed(req, res, project) {
        const decision = await access.decide({
            actor: req.actor,
            action: "project:read",
            resource: { type: "project", companyId: project.companyId, projectId: project.id },
        });
        if (decision.allowed)
            return true;
        res.status(403).json({ error: "Project is outside this actor's authorization boundary" });
        return false;
    }
    async function filterProjectsForActor(req, rows) {
        const decisions = await Promise.all(rows.map((project) => access.decide({
            actor: req.actor,
            action: "project:read",
            resource: { type: "project", companyId: project.companyId, projectId: project.id },
        })));
        return rows.filter((_, index) => decisions[index]?.allowed);
    }
    router.param("id", async (req, _res, next, rawId) => {
        try {
            req.params.id = await normalizeProjectReference(req, rawId);
            next();
        }
        catch (err) {
            next(err);
        }
    });
    router.get("/companies/:companyId/projects", async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const result = await svc.list(companyId);
        res.json(await filterProjectsForActor(req, result));
    });
    router.get("/projects/:id", async (req, res) => {
        const id = req.params.id;
        const project = await svc.getById(id);
        if (!project) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, project.companyId);
        if (!(await assertProjectReadAllowed(req, res, project)))
            return;
        res.json(project);
    });
    router.post("/companies/:companyId/projects", validate(createProjectSchema), async (req, res) => {
        const companyId = req.params.companyId;
        assertCompanyAccess(req, companyId);
        const { workspace, ...projectData } = req.body;
        await assertProjectEnvironmentSelection(companyId, readProjectPolicyEnvironmentId(projectData.executionWorkspacePolicy));
        assertNoAgentHostWorkspaceCommandMutation(req, [
            ...collectProjectExecutionWorkspaceCommandPaths(projectData.executionWorkspacePolicy),
            ...collectProjectWorkspaceCommandPaths(workspace, "workspace"),
        ]);
        if (projectData.env !== undefined) {
            projectData.env = await secretsSvc.normalizeEnvBindingsForPersistence(companyId, projectData.env, { strictMode: strictSecretsMode, fieldPath: "env" });
        }
        const project = await svc.create(companyId, projectData);
        if (project.env) {
            await secretsSvc.syncEnvBindingsForTarget?.(companyId, { targetType: "project", targetId: project.id }, project.env);
        }
        let createdWorkspaceId = null;
        if (workspace) {
            const createdWorkspace = await svc.createWorkspace(project.id, workspace);
            if (!createdWorkspace) {
                await svc.remove(project.id);
                res.status(422).json({ error: "Invalid project workspace payload" });
                return;
            }
            createdWorkspaceId = createdWorkspace.id;
        }
        const hydratedProject = workspace ? await svc.getById(project.id) : project;
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.created",
            entityType: "project",
            entityId: project.id,
            details: {
                name: project.name,
                workspaceId: createdWorkspaceId,
                envKeys: project.env ? Object.keys(project.env).sort() : [],
            },
        });
        const telemetryClient = getTelemetryClient();
        if (telemetryClient) {
            trackProjectCreated(telemetryClient);
        }
        res.status(201).json(hydratedProject ?? project);
    });
    router.patch("/projects/:id", validate(updateProjectSchema), async (req, res) => {
        const id = req.params.id;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        const body = { ...req.body };
        assertNoAgentHostWorkspaceCommandMutation(req, collectProjectExecutionWorkspaceCommandPaths(body.executionWorkspacePolicy));
        await assertProjectEnvironmentSelection(existing.companyId, readProjectPolicyEnvironmentId(body.executionWorkspacePolicy));
        if (typeof body.archivedAt === "string") {
            body.archivedAt = new Date(body.archivedAt);
        }
        if (body.env !== undefined) {
            body.env = await secretsSvc.normalizeEnvBindingsForPersistence(existing.companyId, body.env, {
                strictMode: strictSecretsMode,
                fieldPath: "env",
            });
        }
        const project = await svc.update(id, body);
        if (!project) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        if (body.env !== undefined) {
            await secretsSvc.syncEnvBindingsForTarget?.(project.companyId, { targetType: "project", targetId: project.id }, project.env);
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId: project.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.updated",
            entityType: "project",
            entityId: project.id,
            details: {
                changedKeys: Object.keys(req.body).sort(),
                envKeys: body.env && typeof body.env === "object" && !Array.isArray(body.env)
                    ? Object.keys(body.env).sort()
                    : undefined,
            },
        });
        res.json(project);
    });
    router.get("/projects/:id/workspaces", async (req, res) => {
        const id = req.params.id;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        const workspaces = await svc.listWorkspaces(id);
        res.json(workspaces);
    });
    router.post("/projects/:id/workspaces", validate(createProjectWorkspaceSchema), async (req, res) => {
        const id = req.params.id;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        assertNoAgentHostWorkspaceCommandMutation(req, collectProjectWorkspaceCommandPaths(req.body));
        const workspace = await svc.createWorkspace(id, req.body);
        if (!workspace) {
            res.status(422).json({ error: "Invalid project workspace payload" });
            return;
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId: existing.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.workspace_created",
            entityType: "project",
            entityId: id,
            details: {
                workspaceId: workspace.id,
                name: workspace.name,
                cwd: workspace.cwd,
                isPrimary: workspace.isPrimary,
            },
        });
        res.status(201).json(workspace);
    });
    router.patch("/projects/:id/workspaces/:workspaceId", validate(updateProjectWorkspaceSchema), async (req, res) => {
        const id = req.params.id;
        const workspaceId = req.params.workspaceId;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        assertNoAgentHostWorkspaceCommandMutation(req, collectProjectWorkspaceCommandPaths(req.body));
        const workspaceExists = (await svc.listWorkspaces(id)).some((workspace) => workspace.id === workspaceId);
        if (!workspaceExists) {
            res.status(404).json({ error: "Project workspace not found" });
            return;
        }
        const workspace = await svc.updateWorkspace(id, workspaceId, req.body);
        if (!workspace) {
            res.status(422).json({ error: "Invalid project workspace payload" });
            return;
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId: existing.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.workspace_updated",
            entityType: "project",
            entityId: id,
            details: {
                workspaceId: workspace.id,
                changedKeys: Object.keys(req.body).sort(),
            },
        });
        res.json(workspace);
    });
    async function handleProjectWorkspaceRuntimeCommand(req, res) {
        const id = req.params.id;
        const workspaceId = req.params.workspaceId;
        const action = String(req.params.action ?? "").trim().toLowerCase();
        if (action !== "start" && action !== "stop" && action !== "restart" && action !== "run") {
            res.status(404).json({ error: "Workspace command action not found" });
            return;
        }
        const project = await svc.getById(id);
        if (!project) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, project.companyId);
        const workspace = project.workspaces.find((entry) => entry.id === workspaceId) ?? null;
        if (!workspace) {
            res.status(404).json({ error: "Project workspace not found" });
            return;
        }
        const isSharedWorkspace = Boolean(workspace.sharedWorkspaceKey);
        if (req.actor.type === "agent"
            && isSharedWorkspace
            && SHARED_WORKSPACE_STOP_AND_RESTART_ACTIONS.has(action)) {
            throw forbidden("Missing permission to manage workspace runtime services");
        }
        await assertCanManageProjectWorkspaceRuntimeServices(db, req, {
            companyId: project.companyId,
            projectWorkspaceId: workspace.id,
        });
        const workspaceCwd = workspace.cwd;
        if (!workspaceCwd) {
            res.status(422).json({ error: "Project workspace needs a local path before Paperclip can run workspace commands" });
            return;
        }
        const runtimeConfig = workspace.runtimeConfig?.workspaceRuntime ?? null;
        const target = req.body;
        const configuredServices = runtimeConfig ? listConfiguredRuntimeServiceEntries({ workspaceRuntime: runtimeConfig }) : [];
        const workspaceCommand = runtimeConfig
            ? findWorkspaceCommandDefinition(runtimeConfig, target.workspaceCommandId ?? null)
            : null;
        if (target.workspaceCommandId && !workspaceCommand) {
            res.status(404).json({ error: "Workspace command not found for this project workspace" });
            return;
        }
        if (target.runtimeServiceId && !(workspace.runtimeServices ?? []).some((service) => service.id === target.runtimeServiceId)) {
            res.status(404).json({ error: "Runtime service not found for this project workspace" });
            return;
        }
        const matchedRuntimeService = workspaceCommand?.kind === "service" && !target.runtimeServiceId
            ? matchWorkspaceRuntimeServiceToCommand(workspaceCommand, workspace.runtimeServices ?? [])
            : null;
        const selectedRuntimeServiceId = target.runtimeServiceId ?? matchedRuntimeService?.id ?? null;
        const selectedServiceIndex = workspaceCommand?.kind === "service"
            ? workspaceCommand.serviceIndex
            : target.serviceIndex ?? null;
        if (selectedServiceIndex !== undefined
            && selectedServiceIndex !== null
            && (selectedServiceIndex < 0 || selectedServiceIndex >= configuredServices.length)) {
            res.status(422).json({ error: "Selected runtime service is not defined in this project workspace runtime config" });
            return;
        }
        if (workspaceCommand?.kind === "job" && action !== "run") {
            res.status(422).json({ error: `Workspace job "${workspaceCommand.name}" can only be run` });
            return;
        }
        if (workspaceCommand?.kind === "service" && action === "run") {
            res.status(422).json({ error: `Workspace service "${workspaceCommand.name}" should be started or restarted, not run` });
            return;
        }
        if (action === "run" && !workspaceCommand) {
            res.status(422).json({ error: "Select a workspace job to run" });
            return;
        }
        if ((action === "start" || action === "restart") && !runtimeConfig) {
            res.status(422).json({ error: "Project workspace has no workspace command configuration" });
            return;
        }
        const actor = getActorInfo(req);
        const recorder = workspaceOperations.createRecorder({ companyId: project.companyId });
        let runtimeServiceCount = workspace.runtimeServices?.length ?? 0;
        let stdout = "";
        let stderr = "";
        const operation = await recorder.recordOperation({
            phase: action === "stop" ? "workspace_teardown" : "workspace_provision",
            command: workspaceCommand?.command ?? `workspace command ${action}`,
            cwd: workspace.cwd,
            metadata: {
                action,
                projectId: project.id,
                projectWorkspaceId: workspace.id,
                workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null,
                workspaceCommandKind: workspaceCommand?.kind ?? null,
                workspaceCommandName: workspaceCommand?.name ?? null,
                runtimeServiceId: selectedRuntimeServiceId,
                serviceIndex: selectedServiceIndex,
            },
            run: async () => {
                if (action === "run") {
                    if (!workspaceCommand || workspaceCommand.kind !== "job") {
                        throw new Error("Workspace job selection is required");
                    }
                    return await runWorkspaceJobForControl({
                        actor: {
                            id: actor.agentId ?? null,
                            name: actor.actorType === "user" ? "Board" : "Agent",
                            companyId: project.companyId,
                        },
                        issue: null,
                        workspace: {
                            baseCwd: workspaceCwd,
                            source: "project_primary",
                            projectId: project.id,
                            workspaceId: workspace.id,
                            repoUrl: workspace.repoUrl,
                            repoRef: workspace.repoRef,
                            strategy: "project_primary",
                            cwd: workspaceCwd,
                            branchName: workspace.defaultRef ?? workspace.repoRef ?? null,
                            worktreePath: null,
                            warnings: [],
                            created: false,
                        },
                        command: workspaceCommand.rawConfig,
                        adapterEnv: {},
                        recorder,
                        metadata: {
                            action,
                            projectId: project.id,
                            projectWorkspaceId: workspace.id,
                            workspaceCommandId: workspaceCommand.id,
                        },
                    }).then((nestedOperation) => ({
                        status: "succeeded",
                        exitCode: 0,
                        metadata: {
                            nestedOperationId: nestedOperation?.id ?? null,
                            runtimeServiceCount,
                        },
                    }));
                }
                const onLog = async (stream, chunk) => {
                    if (stream === "stdout")
                        stdout = appendWithCap(stdout, chunk, WORKSPACE_CONTROL_OUTPUT_MAX_CHARS);
                    else
                        stderr = appendWithCap(stderr, chunk, WORKSPACE_CONTROL_OUTPUT_MAX_CHARS);
                };
                if (action === "stop" || action === "restart") {
                    await stopRuntimeServicesForProjectWorkspace({
                        db,
                        projectWorkspaceId: workspace.id,
                        runtimeServiceId: selectedRuntimeServiceId,
                    });
                }
                if (action === "start" || action === "restart") {
                    const startedServices = await startRuntimeServicesForWorkspaceControl({
                        db,
                        actor: {
                            id: actor.agentId ?? null,
                            name: actor.actorType === "user" ? "Board" : "Agent",
                            companyId: project.companyId,
                        },
                        issue: null,
                        workspace: {
                            baseCwd: workspaceCwd,
                            source: "project_primary",
                            projectId: project.id,
                            workspaceId: workspace.id,
                            repoUrl: workspace.repoUrl,
                            repoRef: workspace.repoRef,
                            strategy: "project_primary",
                            cwd: workspaceCwd,
                            branchName: workspace.defaultRef ?? workspace.repoRef ?? null,
                            worktreePath: null,
                            warnings: [],
                            created: false,
                        },
                        config: { workspaceRuntime: runtimeConfig },
                        adapterEnv: {},
                        onLog,
                        serviceIndex: selectedServiceIndex,
                    });
                    runtimeServiceCount = startedServices.length;
                }
                else {
                    runtimeServiceCount = selectedRuntimeServiceId ? Math.max(0, (workspace.runtimeServices?.length ?? 1) - 1) : 0;
                }
                const currentDesiredState = workspace.runtimeConfig?.desiredState
                    ?? ((workspace.runtimeServices ?? []).some((service) => service.status === "starting" || service.status === "running")
                        ? "running"
                        : "stopped");
                const nextRuntimeState = selectedRuntimeServiceId && (selectedServiceIndex === undefined || selectedServiceIndex === null)
                    ? {
                        desiredState: currentDesiredState,
                        serviceStates: workspace.runtimeConfig?.serviceStates ?? null,
                    }
                    : buildWorkspaceRuntimeDesiredStatePatch({
                        config: { workspaceRuntime: runtimeConfig },
                        currentDesiredState,
                        currentServiceStates: workspace.runtimeConfig?.serviceStates ?? null,
                        action,
                        serviceIndex: selectedServiceIndex,
                    });
                await svc.updateWorkspace(project.id, workspace.id, {
                    runtimeConfig: {
                        desiredState: nextRuntimeState.desiredState,
                        serviceStates: nextRuntimeState.serviceStates,
                    },
                });
                return {
                    status: "succeeded",
                    stdout,
                    stderr,
                    system: action === "stop"
                        ? "Stopped project workspace runtime services.\nThis does not pause issue work or held wake scheduling."
                        : action === "restart"
                            ? "Restarted project workspace runtime services.\nThis does not pause issue work or held wake scheduling."
                            : "Started project workspace runtime services.\n",
                    metadata: {
                        runtimeServiceCount,
                        workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null,
                        runtimeServiceId: selectedRuntimeServiceId,
                        serviceIndex: selectedServiceIndex,
                    },
                };
            },
        });
        const updatedWorkspace = (await svc.listWorkspaces(project.id)).find((entry) => entry.id === workspace.id) ?? workspace;
        await logActivity(db, {
            companyId: project.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: `project.workspace_runtime_${action}`,
            entityType: "project",
            entityId: project.id,
            details: {
                projectWorkspaceId: workspace.id,
                runtimeServiceCount,
                workspaceCommandId: workspaceCommand?.id ?? target.workspaceCommandId ?? null,
                workspaceCommandKind: workspaceCommand?.kind ?? null,
                workspaceCommandName: workspaceCommand?.name ?? null,
                runtimeServiceId: selectedRuntimeServiceId,
                serviceIndex: selectedServiceIndex,
            },
        });
        res.json({
            workspace: updatedWorkspace,
            operation,
        });
    }
    router.post("/projects/:id/workspaces/:workspaceId/runtime-services/:action", validate(workspaceRuntimeControlTargetSchema), handleProjectWorkspaceRuntimeCommand);
    router.post("/projects/:id/workspaces/:workspaceId/runtime-commands/:action", validate(workspaceRuntimeControlTargetSchema), handleProjectWorkspaceRuntimeCommand);
    router.delete("/projects/:id/workspaces/:workspaceId", async (req, res) => {
        const id = req.params.id;
        const workspaceId = req.params.workspaceId;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        const workspace = await svc.removeWorkspace(id, workspaceId);
        if (!workspace) {
            res.status(404).json({ error: "Project workspace not found" });
            return;
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId: existing.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.workspace_deleted",
            entityType: "project",
            entityId: id,
            details: {
                workspaceId: workspace.id,
                name: workspace.name,
            },
        });
        res.json(workspace);
    });
    router.delete("/projects/:id", async (req, res) => {
        const id = req.params.id;
        const existing = await svc.getById(id);
        if (!existing) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        assertCompanyAccess(req, existing.companyId);
        const project = await svc.remove(id);
        if (!project) {
            res.status(404).json({ error: "Project not found" });
            return;
        }
        const actor = getActorInfo(req);
        await logActivity(db, {
            companyId: project.companyId,
            actorType: actor.actorType,
            actorId: actor.actorId,
            agentId: actor.agentId,
            action: "project.deleted",
            entityType: "project",
            entityId: project.id,
        });
        res.json(project);
    });
    return router;
}
//# sourceMappingURL=projects.js.map