import { randomUUID } from "node:crypto";
import { ensureOutDir } from "../artifacts/artifacts.js";
import { buildFileInstructions, outboxPath, stageFile, } from "../artifacts/attachments.js";
import { CODEX_AGENT_CAPABILITIES, agentLabel, agentReasoningLabel, agentReasoningOptions, agentSupportsRawAudioAttachments, isAgentId, } from "../agents/shared/agent.js";
import { getExternalSnapshotForSession } from "../agents/shared/agent-activity.js";
import { listAgentAdapterDescriptors } from "../agents/shared/agent-adapter.js";
import { AgentUpdateManager } from "../agents/shared/agent-updates.js";
import { createAgentSessionService, enabledAgents } from "../agents/shared/agent-factory.js";
import { AuditLogStore } from "../access/audit-log.js";
import { BotPreferencesStore } from "../state/bot-preferences.js";
import { ChannelCommandService } from "../channels/shared/channel-command-service.js";
import { ChannelTurnService } from "../channels/shared/channel-turn-service.js";
import { activeSessionSourceForContextKey, ChannelMirrorRegistry } from "../channels/shared/channel-mirror-registry.js";
import { QUEUE_PROMPT_LEASE_TTL_MS, runLeasedQueuedPrompt } from "../channels/shared/channel-prompt-queue.js";
import { listThreads as listCodexThreads } from "../agents/codex/codex-state.js";
import { friendlyErrorText } from "../core/error-messages.js";
import { clearLogFile, getAgentUpdateLogPath, getConnectorHealth, getConnectorLogPath, getPackageVersion, getUpdateLogPath, getVersionChecks, readConnectorState, readFormattedLogTail, spawnConnectorRestart, spawnSelfUpdate } from "../support/operations.js";
import { createCorrelationId, ensurePromptCorrelationId, PromptStore, toPromptEnvelope, webChatAttachmentsForStagedFiles } from "../state/prompt-store.js";
import { UnifiedJobStore } from "../state/job-store.js";
import { buildRuntimeMetrics } from "./metrics.js";
import { RelayArtifactService } from "./relay-artifact-service.js";
import { RelayAuthService } from "./relay-auth-service.js";
import { RelayExternalActivityMonitor } from "./relay-external-activity-monitor.js";
import { RelayQueueService } from "./relay-queue-service.js";
import { RuntimeSnapshotCache } from "./runtime-cache.js";
import { withQueueDrainLock } from "./relay-queue-drain-lock.js";
import { appendQueuedPromptChatMessage, resolveQueuedPromptChatAction } from "./relay-runtime-chat-sync.js";
import { activeSessionPriority, activityToUnifiedJob, agentUpdateStatusToUnified, dedupeJobs, hostLoginCommand, hostLogoutCommand, isExternalSnapshotSuppressedByManagedAbort, isPromptTerminalActivity, normalizeMimeType, promptActivityToUnifiedJob, shouldRefreshActiveSessions, taskToUnifiedJob, uploadFileDtos, } from "./relay-runtime-helpers.js";
import { RelayDashboardService } from "./relay-dashboard-service.js";
import { capabilitiesOf } from "../channels/shared/bot-rendering.js";
import { renderSessionInfoPlain, renderSessionUsageRows } from "../channels/shared/session-format.js";
import { SessionLockStore } from "../access/session-locks.js";
import { SessionRegistry } from "../state/session-registry.js";
import { createSupportBundle } from "../support/support-bundle.js";
import { transcribeAudio } from "../artifacts/voice.js";
import { WebActivityStore, WebChatStore, } from "../web/web-state.js";
import { evaluateWorkspacePolicy, filterAllowedWorkspaces } from "../core/workspace-policy.js";
export const WEB_CONTEXT_KEY = "web:dashboard";
const ACTIVE_CODEX_DISCOVERY_LIMIT = 200;
const ACTIVE_ACTIVITY_TTL_MS = 6 * 60 * 60 * 1000;
const MAX_WEB_SESSION_PAGE_SIZE = 50;
const MAX_CHAT_HISTORY = 250;
const QUEUE_DRAIN_FOLLOW_UP_DELAY_MS = 500;
export async function relayRuntimeSendPrompt(runtime, text, actor, correlationId) {
    const trimmed = text.trim();
    if (!trimmed) {
        throw new Error("Prompt is empty.");
    }
    return runtime.sendEnvelope({ ...toPromptEnvelope(trimmed), correlationId, activityActor: actor }, actor);
}
export async function relayRuntimeSendUploadPrompt(runtime, options, actor) {
    const text = options.text?.trim() ?? "";
    const files = options.files.filter((file) => file.data.byteLength > 0);
    if (!text && files.length === 0) {
        throw new Error("Prompt is empty.");
    }
    const correlationId = options.correlationId?.trim() || createCorrelationId();
    const session = await runtime.getSession(false);
    const sessionInfo = session.getInfo();
    const workspace = sessionInfo.workspace;
    const agentId = sessionInfo.agentId;
    const allowRawAudioAttachments = agentSupportsRawAudioAttachments(agentId);
    const turnId = randomUUID().slice(0, 12);
    const outDir = outboxPath(workspace, turnId);
    await ensureOutDir(outDir);
    const stagedFiles = [];
    const imagePaths = [];
    const transcriptParts = [];
    for (const [index, file] of files.entries()) {
        const mimeType = normalizeMimeType(file.mimeType, file.name);
        const staged = await stageFile(file.data, file.name || `upload-${index + 1}`, mimeType, {
            workspace,
            turnId,
            maxFileSize: runtime.config.maxFileSize,
        });
        stagedFiles.push(staged);
        if (mimeType.startsWith("image/")) {
            imagePaths.push(staged.localPath);
        }
        if (mimeType.startsWith("audio/")) {
            try {
                const result = await transcribeAudio(staged.localPath, {
                    preferredBackend: runtime.config.voicePreferredBackend === "auto"
                        ? undefined
                        : runtime.config.voicePreferredBackend,
                    language: runtime.config.voiceDefaultLanguage,
                });
                const transcript = result.text.trim();
                if (transcript) {
                    transcriptParts.push(`Audio transcript (${staged.safeName}, via ${result.backend}):\n${transcript}`);
                    runtime.appendActivity({
                        source: "web",
                        status: "completed",
                        type: "voice_transcribed",
                        threadId: sessionInfo.threadId,
                        workspace,
                        agentId,
                        actor,
                        correlationId,
                        detail: `${staged.safeName} via ${result.backend}`,
                        durationMs: result.durationMs,
                    });
                }
            }
            catch (error) {
                runtime.appendActivity({
                    source: "web",
                    status: "failed",
                    type: "voice_transcription_failed",
                    threadId: sessionInfo.threadId,
                    workspace,
                    agentId,
                    actor,
                    correlationId,
                    detail: `${staged.safeName}: ${friendlyErrorText(error)}`,
                });
                if (options.transcribeOnly) {
                    throw error;
                }
            }
        }
    }
    if (stagedFiles.length > 0) {
        runtime.appendActivity({
            source: "web",
            status: "info",
            type: "attachment_staged",
            threadId: sessionInfo.threadId,
            workspace,
            agentId,
            actor,
            correlationId,
            detail: `${stagedFiles.length} file(s): ${stagedFiles.map((file) => file.safeName).join(", ")}`,
        });
    }
    const promptAttachmentFiles = stagedFiles.filter((file) => !file.mimeType.startsWith("audio/") || allowRawAudioAttachments);
    const filteredAudioFiles = stagedFiles.filter((file) => file.mimeType.startsWith("audio/") && !allowRawAudioAttachments);
    if (filteredAudioFiles.length > 0) {
        runtime.appendActivity({
            source: "web",
            status: "info",
            type: "attachment_filtered",
            threadId: sessionInfo.threadId,
            workspace,
            agentId,
            actor,
            correlationId,
            detail: `${filteredAudioFiles.length} audio file(s) not sent to ${agentLabel(agentId)}: ${filteredAudioFiles.map((file) => file.safeName).join(", ")}`,
        });
    }
    const audioOnly = stagedFiles.length > 0 && stagedFiles.every((file) => file.mimeType.startsWith("audio/"));
    if (audioOnly && (options.transcribeOnly || (runtime.config.voiceTranscribeOnly && !text))) {
        return {
            queued: false,
            correlationId,
            transcript: transcriptParts.join("\n\n"),
            transcribeOnly: true,
            files: uploadFileDtos(stagedFiles),
        };
    }
    const promptInput = {};
    const textParts = [text, ...transcriptParts].filter(Boolean);
    if (textParts.length > 0) {
        promptInput.text = textParts.join("\n\n");
    }
    if (imagePaths.length > 0) {
        promptInput.imagePaths = imagePaths;
    }
    if (promptAttachmentFiles.length > 0) {
        promptInput.stagedFileInstructions = buildFileInstructions(promptAttachmentFiles, outDir);
    }
    if (textParts.length === 0 && imagePaths.length === 0 && promptAttachmentFiles.length === 0) {
        return {
            queued: false,
            correlationId,
            transcript: transcriptParts.join("\n\n"),
            transcribeOnly: true,
            files: uploadFileDtos(stagedFiles),
        };
    }
    const result = await runtime.sendEnvelope({
        ...toPromptEnvelope(promptInput, outDir),
        attachments: webChatAttachmentsForStagedFiles(stagedFiles, turnId),
        correlationId,
        activityActor: actor,
    }, actor);
    return {
        ...result,
        transcript: transcriptParts.join("\n\n") || undefined,
        files: uploadFileDtos(stagedFiles),
    };
}
export async function relayRuntimeSendEnvelope(runtime, envelope, actor) {
    const activityActor = envelope.activityActor ?? actor;
    const correlated = ensurePromptCorrelationId({ ...envelope, activityActor });
    const session = await runtime.getSession(false);
    const external = getExternalSnapshotForSession(session, runtime.config, { maxEvents: 0 });
    const externalBusy = Boolean(external?.activity.active && !isExternalSnapshotSuppressedByManagedAbort(external, runtime.activityStore.list({ threadId: external.threadId, limit: 50 })));
    if (session.isProcessing() || externalBusy) {
        const queued = runtime.queueService.enqueue(correlated);
        const info = runtime.publicInfo(session);
        appendQueuedPromptChatMessage(runtime, info, queued, runtime.queueService.length());
        runtime.appendActivity({
            source: "web",
            status: "queued",
            type: "prompt_queued",
            threadId: info.threadId,
            workspace: info.workspace,
            agentId: info.agentId,
            actor: activityActor,
            correlationId: correlated.correlationId,
            prompt: correlated.description,
            detail: externalBusy && external
                ? `Queued because ${external.agentLabel} CLI is still processing another task.`
                : `Queued at position ${runtime.queueService.length()}.`,
        });
        runtime.appendAudit({
            action: "prompt_queued",
            status: "ok",
            contextKey: runtime.contextKey,
            agentId: info.agentId,
            threadId: info.threadId,
            workspace: info.workspace,
            promptId: queued.id,
            actor: activityActor,
            correlationId: correlated.correlationId,
            description: correlated.description,
        });
        if (externalBusy && external) {
            runtime.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${runtime.queueService.length()} queued.`, "info");
        }
        runtime.broadcastQueue();
        return { queued: true, queueId: queued.id, correlationId: correlated.correlationId };
    }
    void runtime.runPrompt(session, correlated).catch((error) => {
        runtime.broadcast({ type: "turn_error", id: runtime.currentTurnId ?? "turn", error: friendlyErrorText(error), at: new Date().toISOString(), correlationId: correlated.correlationId });
    });
    return { queued: false, correlationId: correlated.correlationId };
}
export function relayRuntimeQueue(runtime) {
    return runtime.queueService.list();
}
export function relayRuntimeQueuePaused(runtime) {
    return runtime.queueService.isPaused();
}
export function relayRuntimeQueueAction(runtime, action, id, actor) {
    const before = runtime.queueService.rawList();
    const affected = id ? before.find((item) => item.id === id) : undefined;
    runtime.queueService.apply(action, id);
    if (id && action === "cancel" && affected) {
        resolveQueuedPromptChatAction(runtime, id, `Queued prompt ${id} cancelled`);
    }
    if (id && action === "run") {
        void runtime.drainQueue().catch((error) => runtime.broadcastStatus(friendlyErrorText(error), "error"));
    }
    runtime.appendActivity({
        source: "web",
        status: "info",
        type: `queue_${action}`,
        threadId: null,
        workspace: runtime.config.workspace,
        actor,
        correlationId: affected?.correlationId,
        prompt: affected?.description,
        detail: id ? `${action}: ${id}` : `${action}: ${before.length} queued`,
    });
    runtime.appendAudit({
        action: "queue_updated",
        status: "ok",
        contextKey: runtime.contextKey,
        actor,
        correlationId: affected?.correlationId,
        promptId: affected?.id,
        description: id ? `${action}: ${id}` : action,
    });
    runtime.broadcastQueue();
    return runtime.queue();
}
export async function relayRuntimeArtifacts(runtime, limit = 20) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.list(session.getInfo().workspace, limit);
}
export async function relayRuntimeArtifact(runtime, turnId) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.get(session.getInfo().workspace, turnId);
}
export async function relayRuntimeDeleteArtifact(runtime, turnId, actor) {
    const session = await runtime.getSession(true);
    const info = runtime.publicInfo(session);
    const removed = await runtime.artifactService.delete(info.workspace, turnId);
    runtime.appendActivity({
        source: "web",
        status: removed ? "info" : "failed",
        type: "artifact_deleted",
        threadId: info.threadId,
        workspace: info.workspace,
        agentId: info.agentId,
        actor,
        detail: turnId,
    });
    return removed;
}
export async function relayRuntimeCreateArtifactZip(runtime, turnId, actor) {
    const session = await runtime.getSession(true);
    const info = runtime.publicInfo(session);
    const zip = await runtime.artifactService.createZip(info.workspace, turnId);
    if (zip) {
        runtime.appendActivity({
            source: "web",
            status: "info",
            type: "artifact_zip_created",
            threadId: info.threadId,
            workspace: info.workspace,
            agentId: info.agentId,
            actor,
            detail: zip.name,
        });
    }
    return zip;
}
export async function relayRuntimeArtifactPreview(runtime, turnId, relativePath) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.preview(session.getInfo().workspace, turnId, relativePath);
}
export async function relayRuntimeArtifactDiff(runtime, turnId, relativePath) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.diff(session.getInfo().workspace, turnId, relativePath);
}
export async function relayRuntimeArtifactUsage(runtime) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.usage(session.getInfo().workspace);
}
export async function relayRuntimeArtifactCleanupPreview(runtime) {
    const session = await runtime.getSession(true);
    return runtime.artifactService.cleanupPreview(session.getInfo().workspace);
}
export async function relayRuntimeArtifactCleanupRun(runtime, actor) {
    const session = await runtime.getSession(true);
    const info = runtime.publicInfo(session);
    const plan = await runtime.artifactService.cleanupRun(info.workspace);
    runtime.appendActivity({
        source: "web",
        status: "info",
        type: "artifact_cleanup",
        threadId: info.threadId,
        workspace: info.workspace,
        agentId: info.agentId,
        actor,
        detail: `${plan.removedTurnDirs} turns, ${plan.removedInboxDirs} inbox dirs, ${plan.removedBytes} bytes`,
    });
    return plan;
}
export async function relayRuntimeEnsureActiveThread(runtime, session) {
    if (!session.hasActiveThread()) {
        const current = runtime.publicInfo(session);
        let workspace;
        let worktreeId;
        if (runtime.config.sessionWorkspaceMode === "worktree") {
            const worktree = runtime.worktreeService.create({
                agentId: current.agentId,
                contextKey: runtime.contextKey,
                sourceWorkspace: current.workspace,
            });
            workspace = worktree.worktreePath;
            worktreeId = worktree.id;
        }
        const info = await session.newThread(workspace);
        if (worktreeId) {
            runtime.worktreeService.linkThread(worktreeId, info.threadId, info.agentId, runtime.contextKey);
        }
        runtime.updateSession(session);
    }
}
export function relayRuntimeEnsureIdle(runtime, session) {
    if (session.isProcessing()) {
        throw new Error("The active session is still processing a turn.");
    }
}
export async function relayRuntimeRunPrompt(runtime, session, envelope) {
    const workspacePolicy = evaluateWorkspacePolicy(session.getInfo().workspace, runtime.config);
    if (!workspacePolicy.allowed) {
        throw new Error(workspacePolicy.warning ?? "Current workspace is blocked by policy.");
    }
    try {
        await runtime.turnService.run(session, envelope);
    }
    finally {
        await runtime.drainQueue();
    }
}
export async function relayRuntimeDrainQueue(runtime) {
    if (runtime.draining || runtime.queueService.isPaused())
        return;
    let completedPrompt = false;
    const locked = await withQueueDrainLock(runtime, async () => {
        runtime.draining = true;
        const session = await runtime.getSession(false);
        try {
            if (session.isProcessing())
                return;
            const external = getExternalSnapshotForSession(session, runtime.config, { maxEvents: 0 });
            if (external?.activity.active && !isExternalSnapshotSuppressedByManagedAbort(external, runtime.activityStore.list({ threadId: external.threadId, limit: 50 }))) {
                runtime.broadcastStatus(`Waiting for ${external.agentLabel} CLI task... ${runtime.queueService.length()} queued.`, "info");
                return;
            }
            const next = runtime.queueService.leaseNext(runtime.queueDrainOwnerId, QUEUE_PROMPT_LEASE_TTL_MS);
            runtime.broadcastQueue();
            if (!next)
                return;
            resolveQueuedPromptChatAction(runtime, next.id, `Queued prompt ${next.id} started`);
            await runLeasedQueuedPrompt({ renew: () => runtime.queueService.renewLease(next, runtime.queueDrainOwnerId, QUEUE_PROMPT_LEASE_TTL_MS), complete: () => runtime.queueService.completeLease(next, runtime.queueDrainOwnerId), fail: (message) => runtime.queueService.failLease(next, runtime.queueDrainOwnerId, message), run: async () => { await runtime.runPrompt(session, next); completedPrompt = true; } }).finally(() => {
                runtime.broadcastQueue();
            });
        }
        finally {
            runtime.draining = false;
        }
    });
    if (!locked)
        return;
    if (completedPrompt && runtime.queueService.length() > 0 && !runtime.queueService.isPaused()) {
        setTimeout(() => {
            void runtime.drainQueue().catch((error) => runtime.broadcastStatus(friendlyErrorText(error), "error"));
        }, QUEUE_DRAIN_FOLLOW_UP_DELAY_MS).unref?.();
    }
}
export function relayRuntimeUpdateSession(runtime, session) {
    runtime.registry.updateMetadata(runtime.contextKey, session);
    runtime.broadcast({ type: "session_update", session: runtime.publicInfo(session) });
}
