import { randomUUID } from "node:crypto";
import { credentialScopeVersionFromStatus, openAiRuntimeCredentialScopeKey, openAiRuntimeCredentialScopeMetadata, } from "../runtime/credentialScope.js";
import { redactPublicDiagnosticText } from "../security/redactionBoundary.js";
function formatWorkspaceUpsertedOutboxPayload(workspace) {
    return {
        workspace: formatPublicWorkspaceRecord(workspace),
        counts: {
            localRoots: workspace.rootPath.length > 0 ? 1 : 0,
        },
    };
}
function formatRunCreatedOutboxPayload(run) {
    return {
        run: formatPublicRunRecord(run),
        runtime: {
            kind: "opencode",
            session: "created",
        },
        counts: {
            sessionsCreated: 1,
        },
    };
}
function formatRunSubmissionAcceptedOutboxPayload(run, submission) {
    return {
        run: formatPublicRunRecord(run),
        submission: {
            delivery: "accepted",
            textLength: submission.textLength,
            hasModel: submission.hasModel,
        },
        counts: {
            submissionsAccepted: 1,
        },
    };
}
function formatPublicWorkspaceRecord(workspace) {
    return {
        id: workspace.id,
        createdAt: workspace.createdAt,
        updatedAt: workspace.updatedAt,
        storage: {
            localRoot: workspace.rootPath.length > 0 ? "configured" : "missing",
        },
    };
}
function formatPublicRunRecord(run) {
    return {
        id: run.id,
        workspace: { id: run.workspaceId },
        status: run.status,
        ...(run.sourceWorkItemId && run.sourceRepositoryId
            ? {
                source: {
                    workItemId: run.sourceWorkItemId,
                    repositoryId: run.sourceRepositoryId,
                    ...(run.sourcePreparationId
                        ? { preparationId: run.sourcePreparationId }
                        : {}),
                    ...(run.sourceRunIntentId
                        ? { runIntentId: run.sourceRunIntentId }
                        : {}),
                },
            }
            : {}),
        ...(run.branchPlanId && run.branchName && run.baseBranch
            ? {
                branchPlan: {
                    id: run.branchPlanId,
                    name: run.branchName,
                    baseBranch: run.baseBranch,
                },
            }
            : {}),
        ...(run.title ? { title: run.title } : {}),
        ...(run.lastError
            ? { lastError: redactPublicDiagnosticText(run.lastError) }
            : {}),
        createdAt: run.createdAt,
        updatedAt: run.updatedAt,
    };
}
export class PatchTrailRunService {
    localStore;
    runtime;
    audit;
    runtimeEventIngestor;
    providerAuth;
    credentialInjector;
    runtimeCredentialLock;
    constructor(localStore, runtime, audit, runtimeEventIngestor, providerAuth, credentialInjector, runtimeCredentialLock) {
        this.localStore = localStore;
        this.runtime = runtime;
        this.audit = audit;
        this.runtimeEventIngestor = runtimeEventIngestor;
        this.providerAuth = providerAuth;
        this.credentialInjector = credentialInjector;
        this.runtimeCredentialLock = runtimeCredentialLock;
    }
    async createRun(input) {
        const title = input.title ?? input.session?.title ?? "Untitled run";
        const workspace = this.localStore.upsertWorkspace({
            id: input.workspaceId,
            rootPath: input.rootPath,
        });
        this.appendOutbox("workspace.upserted", workspace.id, formatWorkspaceUpsertedOutboxPayload(workspace));
        const runtime = await this.startRuntimeForWorkspace(workspace.id);
        const session = await this.createRuntimeSessionOrStop({
            ...(input.session ?? {}),
            title,
            // Run the agent inside the bound repository working copy when the
            // workspace root is set (locate/clone binds it). An explicit per-call
            // session.directory still wins. The directory is a local path and is
            // never surfaced in any projection.
            ...(workspace.rootPath.length > 0 && !input.session?.directory
                ? { directory: workspace.rootPath }
                : {}),
        });
        const run = this.localStore.createRun({
            id: `run_${randomUUID()}`,
            workspaceId: workspace.id,
            provider: "opencode",
            status: "running",
            title,
            runtimeSessionId: session.id,
            ...(input.sourceBinding
                ? {
                    sourceWorkItemId: input.sourceBinding.workItemId,
                    sourceRepositoryId: input.sourceBinding.repositoryId,
                    sourcePreparationId: input.sourceBinding.preparationId,
                    sourceRunIntentId: input.sourceBinding.runIntentId,
                    branchPlanId: input.sourceBinding.branchPlanId,
                    branchName: input.sourceBinding.branchName,
                    baseBranch: input.sourceBinding.baseBranch,
                }
                : {}),
            ...(input.capPolicy ? { capPolicy: input.capPolicy } : {}),
            ...(input.attemptCount !== undefined
                ? { attemptCount: input.attemptCount }
                : {}),
            ...(input.previousRunId ? { previousRunId: input.previousRunId } : {}),
        });
        this.appendOutbox("run.created", run.id, formatRunCreatedOutboxPayload(run));
        this.audit.append({
            type: "run.created",
            severity: "info",
            message: `Run ${run.id} created for workspace ${workspace.id}.`,
            metadata: {
                runId: run.id,
                workspaceId: workspace.id,
                provider: "opencode",
            },
        });
        this.runtimeEventIngestor?.ensureStarted();
        return { workspace, run, runtime, session };
    }
    async sendPrompt(input) {
        const run = this.localStore.getRun(input.runId);
        if (!run.runtimeSessionId) {
            throw new Error(`Run ${run.id} is missing an OpenCode session id.`);
        }
        await this.runtime.sendPromptAsync(run.runtimeSessionId, {
            ...(input.prompt ?? {}),
            text: input.text,
        });
        this.runtimeEventIngestor?.ensureStarted();
        this.appendOutbox("run.prompt_sent", run.id, formatRunSubmissionAcceptedOutboxPayload(run, {
            textLength: input.text.length,
            hasModel: Boolean(input.prompt?.model),
        }));
        this.audit.append({
            type: "run.prompt_sent",
            severity: "info",
            message: `Prompt accepted for run ${run.id}.`,
            metadata: { runId: run.id, textLength: input.text.length },
        });
        return { run, accepted: true };
    }
    async startRuntimeForWorkspace(workspaceId) {
        const operation = async () => {
            const credentialInjection = await this.buildRuntimeCredentialInjection(workspaceId);
            try {
                return await this.runtime.start(credentialInjection.startOptions);
            }
            finally {
                credentialInjection.cleanup?.();
            }
        };
        if (this.runtimeCredentialLock) {
            return this.runtimeCredentialLock.runExclusive(operation);
        }
        return operation();
    }
    async createRuntimeSessionOrStop(input) {
        try {
            return await this.runtime.createSession(input);
        }
        catch (error) {
            const stopped = await this.runtime.stop();
            if (stopped.state === "failed") {
                throw new Error("Runtime session creation failed and the credential-bearing runtime could not be stopped.");
            }
            throw error;
        }
    }
    async buildRuntimeCredentialInjection(workspaceId) {
        if (!this.providerAuth || !this.credentialInjector)
            return {};
        const status = await this.providerAuth.statusApiKeyInternal(workspaceId, "openai");
        if (status.state !== "available")
            return {};
        let injection;
        try {
            injection = await this.credentialInjector.buildOpenAiEnvironment(status.ref);
        }
        catch {
            throw new Error("OpenAI provider credential is available but could not be prepared for runtime injection.");
        }
        const credentialVersion = credentialScopeVersionFromStatus(status);
        return {
            startOptions: {
                extraEnv: injection.env,
                credentialScopeKey: openAiRuntimeCredentialScopeKey(workspaceId, credentialVersion),
                credentialScope: openAiRuntimeCredentialScopeMetadata({
                    workspaceId,
                    credentialVersion,
                }),
            },
            cleanup: injection.cleanup,
        };
    }
    appendOutbox(type, aggregateId, payload) {
        this.localStore.appendOutboxEvent({
            id: `evt_${randomUUID()}`,
            type,
            aggregateId,
            payload,
        });
    }
}
