import { workspaceRuntimeServices } from "@rudderhq/db";
import { and, desc, eq, inArray } from "drizzle-orm";
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import net from "node:net";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { asNumber, asString, parseObject, renderTemplate } from "../agent-runtimes/utils.js";
import { resolveConfiguredPath, runtimeServiceLeasesByRun, runtimeServicesById, runtimeServicesByReuseKey, sanitizeRuntimeServiceBaseEnv, stableRuntimeServiceId, stableStringify, terminateChildProcess, toRuntimeServiceRef } from "./workspace-runtime.helpers.js";
export async function allocatePort() {
    return await new Promise((resolve, reject) => {
        const server = net.createServer();
        server.listen(0, "127.0.0.1", () => {
            const address = server.address();
            server.close((err) => {
                if (err) {
                    reject(err);
                    return;
                }
                if (!address || typeof address === "string") {
                    reject(new Error("Failed to allocate port"));
                    return;
                }
                resolve(address.port);
            });
        });
        server.on("error", reject);
    });
}
export function buildTemplateData(input) {
    return {
        workspace: {
            cwd: input.workspace.cwd,
            branchName: input.workspace.branchName ?? "",
            worktreePath: input.workspace.worktreePath ?? "",
            repoUrl: input.workspace.repoUrl ?? "",
            repoRef: input.workspace.repoRef ?? "",
            env: input.adapterEnv,
        },
        issue: {
            id: input.issue?.id ?? "",
            identifier: input.issue?.identifier ?? "",
            title: input.issue?.title ?? "",
        },
        agent: {
            id: input.agent.id,
            name: input.agent.name,
        },
        port: input.port ?? "",
    };
}
export function resolveServiceScopeId(input) {
    const scopeTypeRaw = asString(input.service.reuseScope, input.service.lifecycle === "shared" ? "project_workspace" : "run");
    const scopeType = scopeTypeRaw === "project_workspace" ||
        scopeTypeRaw === "execution_workspace" ||
        scopeTypeRaw === "agent"
        ? scopeTypeRaw
        : "run";
    if (scopeType === "project_workspace")
        return { scopeType, scopeId: input.workspace.workspaceId ?? input.workspace.projectId };
    if (scopeType === "execution_workspace") {
        return { scopeType, scopeId: input.executionWorkspaceId ?? input.workspace.cwd };
    }
    if (scopeType === "agent")
        return { scopeType, scopeId: input.agent.id };
    return { scopeType: "run", scopeId: input.runId };
}
export async function waitForReadiness(input) {
    const readiness = parseObject(input.service.readiness);
    const readinessType = asString(readiness.type, "");
    if (readinessType !== "http" || !input.url)
        return;
    const timeoutSec = Math.max(1, asNumber(readiness.timeoutSec, 30));
    const intervalMs = Math.max(100, asNumber(readiness.intervalMs, 500));
    const deadline = Date.now() + timeoutSec * 1000;
    let lastError = "service did not become ready";
    while (Date.now() < deadline) {
        try {
            const response = await fetch(input.url);
            if (response.ok)
                return;
            lastError = `received HTTP ${response.status}`;
        }
        catch (err) {
            lastError = err instanceof Error ? err.message : String(err);
        }
        await delay(intervalMs);
    }
    throw new Error(`Readiness check failed for ${input.url}: ${lastError}`);
}
export function toPersistedWorkspaceRuntimeService(record) {
    return {
        id: record.id,
        orgId: record.orgId,
        projectId: record.projectId,
        projectWorkspaceId: record.projectWorkspaceId,
        executionWorkspaceId: record.executionWorkspaceId,
        issueId: record.issueId,
        scopeType: record.scopeType,
        scopeId: record.scopeId,
        serviceName: record.serviceName,
        status: record.status,
        lifecycle: record.lifecycle,
        reuseKey: record.reuseKey,
        command: record.command,
        cwd: record.cwd,
        port: record.port,
        url: record.url,
        provider: record.provider,
        providerRef: record.providerRef,
        ownerAgentId: record.ownerAgentId,
        startedByRunId: record.startedByRunId,
        lastUsedAt: new Date(record.lastUsedAt),
        startedAt: new Date(record.startedAt),
        stoppedAt: record.stoppedAt ? new Date(record.stoppedAt) : null,
        stopPolicy: record.stopPolicy,
        healthStatus: record.healthStatus,
        updatedAt: new Date(),
    };
}
export async function persistRuntimeServiceRecord(db, record) {
    if (!db)
        return;
    const values = toPersistedWorkspaceRuntimeService(record);
    await db
        .insert(workspaceRuntimeServices)
        .values(values)
        .onConflictDoUpdate({
        target: workspaceRuntimeServices.id,
        set: {
            projectId: values.projectId,
            projectWorkspaceId: values.projectWorkspaceId,
            executionWorkspaceId: values.executionWorkspaceId,
            issueId: values.issueId,
            scopeType: values.scopeType,
            scopeId: values.scopeId,
            serviceName: values.serviceName,
            status: values.status,
            lifecycle: values.lifecycle,
            reuseKey: values.reuseKey,
            command: values.command,
            cwd: values.cwd,
            port: values.port,
            url: values.url,
            provider: values.provider,
            providerRef: values.providerRef,
            ownerAgentId: values.ownerAgentId,
            startedByRunId: values.startedByRunId,
            lastUsedAt: values.lastUsedAt,
            startedAt: values.startedAt,
            stoppedAt: values.stoppedAt,
            stopPolicy: values.stopPolicy,
            healthStatus: values.healthStatus,
            updatedAt: values.updatedAt,
        },
    });
}
export function clearIdleTimer(record) {
    if (!record.idleTimer)
        return;
    clearTimeout(record.idleTimer);
    record.idleTimer = null;
}
export function normalizeAdapterManagedRuntimeServices(input) {
    const nowIso = (input.now ?? new Date()).toISOString();
    return input.reports.map((report) => {
        const scopeType = report.scopeType ?? "run";
        const scopeId = report.scopeId ??
            (scopeType === "project_workspace"
                ? input.workspace.workspaceId
                : scopeType === "execution_workspace"
                    ? input.executionWorkspaceId ?? input.workspace.cwd
                    : scopeType === "agent"
                        ? input.agent.id
                        : input.runId) ??
            null;
        const serviceName = asString(report.serviceName, "").trim() || "service";
        const status = report.status ?? "running";
        const lifecycle = report.lifecycle ?? "ephemeral";
        const healthStatus = report.healthStatus ??
            (status === "running" ? "healthy" : status === "failed" ? "unhealthy" : "unknown");
        return {
            id: stableRuntimeServiceId({
                agentRuntimeType: input.agentRuntimeType,
                runId: input.runId,
                scopeType,
                scopeId,
                serviceName,
                reportId: report.id ?? null,
                providerRef: report.providerRef ?? null,
                reuseKey: report.reuseKey ?? null,
            }),
            orgId: input.agent.orgId,
            projectId: report.projectId ?? input.workspace.projectId,
            projectWorkspaceId: report.projectWorkspaceId ?? input.workspace.workspaceId,
            executionWorkspaceId: input.executionWorkspaceId ?? null,
            issueId: report.issueId ?? input.issue?.id ?? null,
            serviceName,
            status,
            lifecycle,
            scopeType,
            scopeId,
            reuseKey: report.reuseKey ?? null,
            command: report.command ?? null,
            cwd: report.cwd ?? null,
            port: report.port ?? null,
            url: report.url ?? null,
            provider: "adapter_managed",
            providerRef: report.providerRef ?? null,
            ownerAgentId: report.ownerAgentId ?? input.agent.id,
            startedByRunId: input.runId,
            lastUsedAt: nowIso,
            startedAt: nowIso,
            stoppedAt: status === "running" || status === "starting" ? null : nowIso,
            stopPolicy: report.stopPolicy ?? null,
            healthStatus,
            reused: false,
        };
    });
}
export async function startLocalRuntimeService(input) {
    const serviceName = asString(input.service.name, "service");
    const lifecycle = asString(input.service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
    const command = asString(input.service.command, "");
    if (!command)
        throw new Error(`Runtime service "${serviceName}" is missing command`);
    const serviceCwdTemplate = asString(input.service.cwd, ".");
    const portConfig = parseObject(input.service.port);
    const port = asString(portConfig.type, "") === "auto" ? await allocatePort() : null;
    const envConfig = parseObject(input.service.env);
    const templateData = buildTemplateData({
        workspace: input.workspace,
        agent: input.agent,
        issue: input.issue,
        adapterEnv: input.adapterEnv,
        port,
    });
    const serviceCwd = resolveConfiguredPath(renderTemplate(serviceCwdTemplate, templateData), input.workspace.cwd);
    const env = {
        ...sanitizeRuntimeServiceBaseEnv(process.env),
        ...input.adapterEnv,
    };
    for (const [key, value] of Object.entries(envConfig)) {
        if (typeof value === "string") {
            env[key] = renderTemplate(value, templateData);
        }
    }
    if (port) {
        const portEnvKey = asString(portConfig.envKey, "PORT");
        env[portEnvKey] = String(port);
    }
    const shell = process.env.SHELL?.trim() || "/bin/sh";
    const child = spawn(shell, ["-lc", command], {
        cwd: serviceCwd,
        env,
        detached: process.platform !== "win32",
        stdio: ["ignore", "pipe", "pipe"],
    });
    let stderrExcerpt = "";
    let stdoutExcerpt = "";
    child.stdout?.on("data", async (chunk) => {
        const text = String(chunk);
        stdoutExcerpt = (stdoutExcerpt + text).slice(-4096);
        if (input.onLog)
            await input.onLog("stdout", `[service:${serviceName}] ${text}`);
    });
    child.stderr?.on("data", async (chunk) => {
        const text = String(chunk);
        stderrExcerpt = (stderrExcerpt + text).slice(-4096);
        if (input.onLog)
            await input.onLog("stderr", `[service:${serviceName}] ${text}`);
    });
    const expose = parseObject(input.service.expose);
    const readiness = parseObject(input.service.readiness);
    const urlTemplate = asString(expose.urlTemplate, "") ||
        asString(readiness.urlTemplate, "");
    const url = urlTemplate ? renderTemplate(urlTemplate, templateData) : null;
    try {
        await waitForReadiness({ service: input.service, url });
    }
    catch (err) {
        terminateChildProcess(child);
        throw new Error(`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`);
    }
    const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
    return {
        id: randomUUID(),
        orgId: input.agent.orgId,
        projectId: input.workspace.projectId,
        projectWorkspaceId: input.workspace.workspaceId,
        executionWorkspaceId: input.executionWorkspaceId ?? null,
        issueId: input.issue?.id ?? null,
        serviceName,
        status: "running",
        lifecycle,
        scopeType: input.scopeType,
        scopeId: input.scopeId,
        reuseKey: input.reuseKey,
        command,
        cwd: serviceCwd,
        port,
        url,
        provider: "local_process",
        providerRef: child.pid ? String(child.pid) : null,
        ownerAgentId: input.agent.id,
        startedByRunId: input.runId,
        lastUsedAt: new Date().toISOString(),
        startedAt: new Date().toISOString(),
        stoppedAt: null,
        stopPolicy: parseObject(input.service.stopPolicy),
        healthStatus: "healthy",
        reused: false,
        db: input.db,
        child,
        leaseRunIds: new Set([input.runId]),
        idleTimer: null,
        envFingerprint,
    };
}
export function scheduleIdleStop(record) {
    clearIdleTimer(record);
    const stopType = asString(record.stopPolicy?.type, "manual");
    if (stopType !== "idle_timeout")
        return;
    const idleSeconds = Math.max(1, asNumber(record.stopPolicy?.idleSeconds, 1800));
    record.idleTimer = setTimeout(() => {
        stopRuntimeService(record.id).catch(() => undefined);
    }, idleSeconds * 1000);
}
export async function stopRuntimeService(serviceId) {
    const record = runtimeServicesById.get(serviceId);
    if (!record)
        return;
    clearIdleTimer(record);
    record.status = "stopped";
    record.lastUsedAt = new Date().toISOString();
    record.stoppedAt = new Date().toISOString();
    if (record.child && record.child.pid) {
        terminateChildProcess(record.child);
    }
    runtimeServicesById.delete(serviceId);
    if (record.reuseKey) {
        runtimeServicesByReuseKey.delete(record.reuseKey);
    }
    await persistRuntimeServiceRecord(record.db, record);
}
export async function markPersistedRuntimeServicesStoppedForExecutionWorkspace(input) {
    const now = new Date();
    await input.db
        .update(workspaceRuntimeServices)
        .set({
        status: "stopped",
        healthStatus: "unknown",
        stoppedAt: now,
        lastUsedAt: now,
        updatedAt: now,
    })
        .where(and(eq(workspaceRuntimeServices.executionWorkspaceId, input.executionWorkspaceId), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
}
export function registerRuntimeService(db, record) {
    record.db = db;
    runtimeServicesById.set(record.id, record);
    if (record.reuseKey) {
        runtimeServicesByReuseKey.set(record.reuseKey, record.id);
    }
    record.child?.on("exit", (code, signal) => {
        const current = runtimeServicesById.get(record.id);
        if (!current)
            return;
        clearIdleTimer(current);
        current.status = code === 0 || signal === "SIGTERM" ? "stopped" : "failed";
        current.healthStatus = current.status === "failed" ? "unhealthy" : "unknown";
        current.lastUsedAt = new Date().toISOString();
        current.stoppedAt = new Date().toISOString();
        runtimeServicesById.delete(current.id);
        if (current.reuseKey && runtimeServicesByReuseKey.get(current.reuseKey) === current.id) {
            runtimeServicesByReuseKey.delete(current.reuseKey);
        }
        void persistRuntimeServiceRecord(db, current);
    });
}
export async function ensureRuntimeServicesForRun(input) {
    const runtime = parseObject(input.config.workspaceRuntime);
    const rawServices = Array.isArray(runtime.services)
        ? runtime.services.filter((entry) => typeof entry === "object" && entry !== null)
        : [];
    const acquiredServiceIds = [];
    const refs = [];
    runtimeServiceLeasesByRun.set(input.runId, acquiredServiceIds);
    try {
        for (const service of rawServices) {
            const lifecycle = asString(service.lifecycle, "shared") === "ephemeral" ? "ephemeral" : "shared";
            const { scopeType, scopeId } = resolveServiceScopeId({
                service,
                workspace: input.workspace,
                executionWorkspaceId: input.executionWorkspaceId,
                issue: input.issue,
                runId: input.runId,
                agent: input.agent,
            });
            const envConfig = parseObject(service.env);
            const envFingerprint = createHash("sha256").update(stableStringify(envConfig)).digest("hex");
            const serviceName = asString(service.name, "service");
            const reuseKey = lifecycle === "shared"
                ? [scopeType, scopeId ?? "", serviceName, envFingerprint].join(":")
                : null;
            if (reuseKey) {
                const existingId = runtimeServicesByReuseKey.get(reuseKey);
                const existing = existingId ? runtimeServicesById.get(existingId) : null;
                if (existing && existing.status === "running") {
                    existing.leaseRunIds.add(input.runId);
                    existing.lastUsedAt = new Date().toISOString();
                    existing.stoppedAt = null;
                    clearIdleTimer(existing);
                    await persistRuntimeServiceRecord(input.db, existing);
                    acquiredServiceIds.push(existing.id);
                    refs.push(toRuntimeServiceRef(existing, { reused: true }));
                    continue;
                }
            }
            const record = await startLocalRuntimeService({
                db: input.db,
                runId: input.runId,
                agent: input.agent,
                issue: input.issue,
                workspace: input.workspace,
                executionWorkspaceId: input.executionWorkspaceId,
                adapterEnv: input.adapterEnv,
                service,
                onLog: input.onLog,
                reuseKey,
                scopeType,
                scopeId,
            });
            registerRuntimeService(input.db, record);
            await persistRuntimeServiceRecord(input.db, record);
            acquiredServiceIds.push(record.id);
            refs.push(toRuntimeServiceRef(record));
        }
    }
    catch (err) {
        await releaseRuntimeServicesForRun(input.runId);
        throw err;
    }
    return refs;
}
export async function releaseRuntimeServicesForRun(runId) {
    const acquired = runtimeServiceLeasesByRun.get(runId) ?? [];
    runtimeServiceLeasesByRun.delete(runId);
    for (const serviceId of acquired) {
        const record = runtimeServicesById.get(serviceId);
        if (!record)
            continue;
        record.leaseRunIds.delete(runId);
        record.lastUsedAt = new Date().toISOString();
        const stopType = asString(record.stopPolicy?.type, record.lifecycle === "ephemeral" ? "on_run_finish" : "manual");
        await persistRuntimeServiceRecord(record.db, record);
        if (record.leaseRunIds.size === 0) {
            if (record.lifecycle === "ephemeral" || stopType === "on_run_finish") {
                await stopRuntimeService(serviceId);
                continue;
            }
            scheduleIdleStop(record);
        }
    }
}
export async function stopRuntimeServicesForExecutionWorkspace(input) {
    const normalizedWorkspaceCwd = input.workspaceCwd ? path.resolve(input.workspaceCwd) : null;
    const matchingServiceIds = Array.from(runtimeServicesById.values())
        .filter((record) => {
        if (record.executionWorkspaceId === input.executionWorkspaceId)
            return true;
        if (!normalizedWorkspaceCwd || !record.cwd)
            return false;
        const resolvedCwd = path.resolve(record.cwd);
        return (resolvedCwd === normalizedWorkspaceCwd ||
            resolvedCwd.startsWith(`${normalizedWorkspaceCwd}${path.sep}`));
    })
        .map((record) => record.id);
    for (const serviceId of matchingServiceIds) {
        await stopRuntimeService(serviceId);
    }
    if (input.db) {
        await markPersistedRuntimeServicesStoppedForExecutionWorkspace({
            db: input.db,
            executionWorkspaceId: input.executionWorkspaceId,
        });
    }
}
export async function listWorkspaceRuntimeServicesForProjectWorkspaces(db, orgId, projectWorkspaceIds) {
    if (projectWorkspaceIds.length === 0)
        return new Map();
    const rows = await db
        .select()
        .from(workspaceRuntimeServices)
        .where(and(eq(workspaceRuntimeServices.orgId, orgId), inArray(workspaceRuntimeServices.projectWorkspaceId, projectWorkspaceIds)))
        .orderBy(desc(workspaceRuntimeServices.updatedAt), desc(workspaceRuntimeServices.createdAt));
    const grouped = new Map();
    for (const row of rows) {
        if (!row.projectWorkspaceId)
            continue;
        const existing = grouped.get(row.projectWorkspaceId);
        if (existing)
            existing.push(row);
        else
            grouped.set(row.projectWorkspaceId, [row]);
    }
    return grouped;
}
export async function reconcilePersistedRuntimeServicesOnStartup(db) {
    const staleRows = await db
        .select({ id: workspaceRuntimeServices.id })
        .from(workspaceRuntimeServices)
        .where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
    if (staleRows.length === 0)
        return { reconciled: 0 };
    const now = new Date();
    await db
        .update(workspaceRuntimeServices)
        .set({
        status: "stopped",
        healthStatus: "unknown",
        stoppedAt: now,
        lastUsedAt: now,
        updatedAt: now,
    })
        .where(and(eq(workspaceRuntimeServices.provider, "local_process"), inArray(workspaceRuntimeServices.status, ["starting", "running"])));
    return { reconciled: staleRows.length };
}
export async function persistAdapterManagedRuntimeServices(input) {
    const refs = normalizeAdapterManagedRuntimeServices(input);
    if (refs.length === 0)
        return refs;
    const existingRows = await input.db
        .select()
        .from(workspaceRuntimeServices)
        .where(inArray(workspaceRuntimeServices.id, refs.map((ref) => ref.id)));
    const existingById = new Map(existingRows.map((row) => [row.id, row]));
    for (const ref of refs) {
        const existing = existingById.get(ref.id);
        const startedAt = existing?.startedAt ?? new Date(ref.startedAt);
        const createdAt = existing?.createdAt ?? new Date();
        await input.db
            .insert(workspaceRuntimeServices)
            .values({
            id: ref.id,
            orgId: ref.orgId,
            projectId: ref.projectId,
            projectWorkspaceId: ref.projectWorkspaceId,
            executionWorkspaceId: ref.executionWorkspaceId,
            issueId: ref.issueId,
            scopeType: ref.scopeType,
            scopeId: ref.scopeId,
            serviceName: ref.serviceName,
            status: ref.status,
            lifecycle: ref.lifecycle,
            reuseKey: ref.reuseKey,
            command: ref.command,
            cwd: ref.cwd,
            port: ref.port,
            url: ref.url,
            provider: ref.provider,
            providerRef: ref.providerRef,
            ownerAgentId: ref.ownerAgentId,
            startedByRunId: ref.startedByRunId,
            lastUsedAt: new Date(ref.lastUsedAt),
            startedAt,
            stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
            stopPolicy: ref.stopPolicy,
            healthStatus: ref.healthStatus,
            createdAt,
            updatedAt: new Date(),
        })
            .onConflictDoUpdate({
            target: workspaceRuntimeServices.id,
            set: {
                projectId: ref.projectId,
                projectWorkspaceId: ref.projectWorkspaceId,
                executionWorkspaceId: ref.executionWorkspaceId,
                issueId: ref.issueId,
                scopeType: ref.scopeType,
                scopeId: ref.scopeId,
                serviceName: ref.serviceName,
                status: ref.status,
                lifecycle: ref.lifecycle,
                reuseKey: ref.reuseKey,
                command: ref.command,
                cwd: ref.cwd,
                port: ref.port,
                url: ref.url,
                provider: ref.provider,
                providerRef: ref.providerRef,
                ownerAgentId: ref.ownerAgentId,
                startedByRunId: ref.startedByRunId,
                lastUsedAt: new Date(ref.lastUsedAt),
                startedAt,
                stoppedAt: ref.stoppedAt ? new Date(ref.stoppedAt) : null,
                stopPolicy: ref.stopPolicy,
                healthStatus: ref.healthStatus,
                updatedAt: new Date(),
            },
        });
    }
    return refs;
}
//# sourceMappingURL=workspace-runtime.services.js.map