/* eslint-disable max-classes-per-file, @typescript-eslint/no-invalid-void-type, class-methods-use-this, @typescript-eslint/parameter-properties */
/**
 * Convex-backed Flue {@link PersistenceAdapter} for the agents package.
 *
 * Every store method maps to a public Convex function exported from
 * `fluePersistence.ts` under the `fluePersistence:<name>` path. The adapter
 * never imports generated backend bindings — it uses `makeFunctionReference` to
 * build typed references against explicit DTO shapes, so the file typechecks
 * against the installed runtime package without depending on codegen output.
 *
 * DTO conventions shared with the backend:
 *   - Submissions persist `inputJson` (the dispatch input or the direct input
 *     with image data stripped via `prepareDirectSubmission`) plus `chunksJson`
 *     for direct submissions (a `PersistedImageChunk[]`). Direct inputs are
 *     rehydrated with `hydratePersistedDirectSubmission`.
 *   - Conversation batches persist a numeric `offset` and `recordsJson`.
 *   - Event entries persist a numeric `offset` and `dataJson`.
 *   - Attachments carry `attachmentJson` and raw `bytes` as `ArrayBuffer`.
 *   - Run records persist `inputJson`/`resultJson`/`errorJson`/`traceCarrierJson`.
 *   - Append functions return `{ offset: number; appended: boolean }`; the
 *     client formats the offset into Flue's DS-compatible string and notifies
 *     in-process listeners only when `appended` is true (idempotent retries do
 *     not double-fire).
 *   - Read functions take a numeric `afterOffset` (parsed from the Flue offset
 *     string by the client); list functions take a decoded cursor.
 */

import { env } from "@code/env/server";
import { AttachmentConflictError, DEFAULT_LIST_LIMIT, DEFAULT_READ_LIMIT, MAX_LIST_LIMIT, MAX_READ_LIMIT, StreamListenerRegistry, assertSupportedFlueSchemaVersion, clampLimit, copyAttachmentBytes, createSessionStorageKey, decodeRunCursor, encodeRunCursor, formatOffset, hydratePersistedDirectSubmission, parseOffset, prepareDirectSubmission, verifyAttachmentBytes } from '@flue/runtime/adapter';
import type { AgentAttemptMarker, AgentDispatchAdmission, AgentDispatchReceipt, AgentExecutionStore, AgentSubmission, AgentSubmissionStore, AttachmentRef, AttachmentStore, ConversationProducerClaim, ConversationRecord, ConversationStreamBatch, ConversationStreamIdentity, ConversationStreamMeta, ConversationStreamReadResult, ConversationStreamStore, CreateRunInput, DirectAgentSubmissionInput, DispatchAgentSubmissionInput, DispatchInput, EndRunInput, EventStreamMeta, EventStreamReadResult, EventStreamStore, GetAttachmentInput, PersistedChunkRow, PersistenceAdapter, PersistenceStores, PutAttachmentInput, RunPointer, RunRecord, RunStore, RunStatus, StoredAttachment, SubmissionAttemptRef, SubmissionClaimRef, SubmissionDurability, SubmissionSettlementObligation, SubmissionSettledRecord } from '@flue/runtime/adapter';
import { ConvexHttpClient } from "convex/browser";
import { makeFunctionReference } from 'convex/server';
import type { FunctionArgs, FunctionReference, FunctionReturnType } from 'convex/server';

// ---------------------------------------------------------------------------
// Token + arg types
//
// All Convex arg types are declared as type aliases (not interfaces) so they
// satisfy Convex's `DefaultFunctionArgs = Record<string, unknown>` constraint.
// TypeScript does not give interfaces an implicit string index signature, but
// object-literal type aliases do.
// ---------------------------------------------------------------------------

/** Auth token present on every Convex function call. */
interface TokenArgs { readonly token: string; readonly [key: string]: unknown }

const TOKEN = (): string => env.FLUE_DB_TOKEN;

/** Trace carrier shape (W3C tracecontext). Not exported by Flue's adapter. */
type TraceCarrier = NonNullable<DirectAgentSubmissionInput["traceCarrier"]>;

// ---------------------------------------------------------------------------
// Shared DTO shapes (wire types returned by the Convex backend)
// ---------------------------------------------------------------------------

/** Common fields stored for every submission row. */
interface SubmissionRow {
  readonly sequence: number;
  readonly submissionId: string;
  readonly sessionKey: string;
  readonly kind: "dispatch" | "direct";
  readonly inputJson: string;
  readonly chunksJson: string | null;
  readonly status: "queued" | "running" | "terminalizing" | "settled";
  readonly acceptedAt: number;
  readonly canonicalReadyAt: number | null;
  readonly attemptId: string | null;
  readonly inputAppliedAt: number | null;
  readonly recoveryRequestedAt: number | null;
  readonly abortRequestedAt: number | null;
  readonly startedAt: number | null;
  readonly error: string | null;
  readonly attemptCount: number;
  readonly maxRetry: number;
  readonly timeoutAt: number;
  readonly ownerId: string | null;
  readonly leaseExpiresAt: number;
  readonly traceCarrierJson: string | null;
}

interface SettlementObligationRow {
  readonly submissionId: string;
  readonly sessionKey: string;
  readonly attemptId: string;
  readonly recordId: string;
  readonly recordJson: string;
}

interface AttemptMarkerRow {
  readonly submissionId: string;
  readonly attemptId: string;
  readonly createdAt: number;
}

interface ConversationStreamRow {
  readonly identity: ConversationStreamIdentity;
  readonly incarnation: string;
  readonly nextOffset: number;
  readonly producerId: string | null;
  readonly producerEpoch: number;
  readonly nextProducerSequence: number;
}

interface ConversationBatchRow {
  readonly offset: number;
  readonly recordsJson: string;
}

interface EventEntryRow {
  readonly offset: number;
  readonly dataJson: string;
}

interface RunRow {
  readonly runId: string;
  readonly workflowName: string;
  readonly status: RunStatus;
  readonly startedAt: string;
  readonly endedAt: string | null;
  readonly isError: boolean | null;
  readonly durationMs: number | null;
  readonly inputJson: string | null;
  readonly resultJson: string | null;
  readonly errorJson: string | null;
  readonly traceCarrierJson: string | null;
}

interface RunPointerRow {
  readonly runId: string;
  readonly workflowName: string;
  readonly status: RunStatus;
  readonly startedAt: string;
  readonly endedAt: string | null;
  readonly durationMs: number | null;
  readonly isError: boolean | null;
}

interface AttachmentRefWire {
  readonly id: string;
  readonly mimeType: string;
  readonly size: number;
  readonly digest: string;
  readonly filename?: string;
}

// ---------------------------------------------------------------------------
// Typed function references
//
// `makeFunctionReference` builds a typed reference to a public Convex function
// by name. We declare explicit arg/return DTOs so the client side typechecks
// without importing generated backend bindings.
// ---------------------------------------------------------------------------

const checkSchemaVersion = makeFunctionReference<"query", TokenArgs, string>(
  "fluePersistence:checkSchemaVersion",
);

const getSubmission = makeFunctionReference<
  "query",
  TokenArgs & { readonly submissionId: string },
  SubmissionRow | null
>("fluePersistence:getSubmission");

const hasUnsettledSubmissions = makeFunctionReference<
  "query",
  TokenArgs,
  boolean
>("fluePersistence:hasUnsettledSubmissions");

const listRunnableSubmissions = makeFunctionReference<
  "query",
  TokenArgs,
  SubmissionRow[]
>("fluePersistence:listRunnableSubmissions");

const listUnreadySubmissions = makeFunctionReference<
  "query",
  TokenArgs,
  SubmissionRow[]
>("fluePersistence:listUnreadySubmissions");

const listRunningSubmissions = makeFunctionReference<
  "query",
  TokenArgs,
  SubmissionRow[]
>("fluePersistence:listRunningSubmissions");

const listPendingSubmissionSettlements = makeFunctionReference<
  "query",
  TokenArgs,
  SettlementObligationRow[]
>("fluePersistence:listPendingSubmissionSettlements");

type ReplaceAttemptArgs = TokenArgs & {
  readonly submissionId: string;
  readonly attemptId: string;
  readonly nextAttemptId: string;
  readonly ownerId?: string;
  readonly leaseExpiresAt?: number;
};
const replaceSubmissionAttempt = makeFunctionReference<
  "mutation",
  ReplaceAttemptArgs,
  SubmissionRow | null
>("fluePersistence:replaceSubmissionAttempt");

/** Unified admission envelope — `kind` discriminates dispatch vs direct. */
interface AdmitSubmissionEnvelope {
  readonly kind: "dispatch" | "direct";
  readonly submissionId: string;
  readonly sessionKey: string;
  readonly acceptedAt: number;
  readonly inputJson: string;
  readonly chunksJson?: string;
  readonly traceCarrierJson?: string;
}
interface AdmitSubmissionResponse {
  readonly kind: "submission" | "retained_receipt" | "conflict";
  readonly submission?: SubmissionRow;
  readonly receipt?: AgentDispatchReceipt;
}
const admitSubmission = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly input: AdmitSubmissionEnvelope },
  AdmitSubmissionResponse
>("fluePersistence:admitSubmission");

const markSubmissionCanonicalReady = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly submissionId: string },
  SubmissionRow | null
>("fluePersistence:markSubmissionCanonicalReady");

type ClaimArgs = TokenArgs & SubmissionClaimRef;
const claimSubmission = makeFunctionReference<
  "mutation",
  ClaimArgs,
  SubmissionRow | null
>("fluePersistence:claimSubmission");

type MarkInputAppliedArgs = TokenArgs &
  SubmissionAttemptRef & {
    readonly maxRetry?: number;
    readonly timeoutAt?: number;
  };
const markSubmissionInputApplied = makeFunctionReference<
  "mutation",
  MarkInputAppliedArgs,
  boolean
>("fluePersistence:markSubmissionInputApplied");

type AttemptArgs = TokenArgs & SubmissionAttemptRef;
const requestSubmissionRecovery = makeFunctionReference<
  "mutation",
  AttemptArgs,
  boolean
>("fluePersistence:requestSubmissionRecovery");

const requestSessionAbort = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly sessionKey: string },
  string[]
>("fluePersistence:requestSessionAbort");

const requeueSubmissionBeforeInputApplied = makeFunctionReference<
  "mutation",
  AttemptArgs,
  boolean
>("fluePersistence:requeueSubmissionBeforeInputApplied");

type ReserveSettlementArgs = TokenArgs &
  SubmissionAttemptRef & {
    readonly recordId: string;
    readonly recordJson: string;
  };
const reserveSubmissionSettlement = makeFunctionReference<
  "mutation",
  ReserveSettlementArgs,
  SettlementObligationRow | null
>("fluePersistence:reserveSubmissionSettlement");

const finalizeSubmissionSettlement = makeFunctionReference<
  "mutation",
  AttemptArgs & { readonly recordId: string },
  boolean
>("fluePersistence:finalizeSubmissionSettlement");

type SettleArgs = TokenArgs &
  SubmissionAttemptRef & {
    readonly outcome: "completed" | "failed";
    readonly errorJson?: string;
  };
const settleSubmission = makeFunctionReference<
  "mutation",
  SettleArgs,
  boolean
>("fluePersistence:settleSubmission");

const insertAttemptMarker = makeFunctionReference<
  "mutation",
  AttemptArgs,
  AttemptMarkerRow
>("fluePersistence:insertAttemptMarker");

const deleteAttemptMarker = makeFunctionReference<
  "mutation",
  AttemptArgs,
  void
>("fluePersistence:deleteAttemptMarker");

const listAttemptMarkers = makeFunctionReference<
  "query",
  TokenArgs,
  AttemptMarkerRow[]
>("fluePersistence:listAttemptMarkers");

const renewLeases = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly ownerId: string; readonly submissionIds: string[] },
  void
>("fluePersistence:renewLeases");

const listExpiredSubmissions = makeFunctionReference<
  "query",
  TokenArgs,
  SubmissionRow[]
>("fluePersistence:listExpiredSubmissions");

// Conversation stream
type CreateStreamArgs = TokenArgs & {
  readonly path: string;
  readonly identity: ConversationStreamIdentity;
};
const createConversationStream = makeFunctionReference<
  "mutation",
  CreateStreamArgs,
  void
>("fluePersistence:createConversationStream");

const acquireConversationProducer = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly path: string; readonly producerId: string },
  ConversationProducerClaim
>("fluePersistence:acquireConversationProducer");

type AppendBatchArgs = TokenArgs & {
  readonly path: string;
  readonly producerId: string;
  readonly producerEpoch: number;
  readonly incarnation: string;
  readonly producerSequence: number;
  readonly submission?: { readonly submissionId: string; readonly attemptId: string };
  readonly recordsJson: string;
};
interface AppendResult { readonly offset: number; readonly appended: boolean }
const appendConversationBatch = makeFunctionReference<
  "mutation",
  AppendBatchArgs,
  AppendResult
>("fluePersistence:appendConversationBatch");

type ReadBatchesArgs = TokenArgs & {
  readonly path: string;
  readonly afterOffset: number;
  readonly limit: number;
};
interface ReadBatchesResult {
  readonly batches: ConversationBatchRow[];
  readonly nextOffset: number;
  readonly upToDate: boolean;
}
const readConversationBatches = makeFunctionReference<
  "query",
  ReadBatchesArgs,
  ReadBatchesResult
>("fluePersistence:readConversationBatches");

const getConversationStreamMeta = makeFunctionReference<
  "query",
  TokenArgs & { readonly path: string },
  ConversationStreamRow | null
>("fluePersistence:getConversationStreamMeta");

const deleteConversationStream = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly path: string },
  void
>("fluePersistence:deleteConversationStream");

// Event stream
const createEventStream = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly path: string },
  void
>("fluePersistence:createEventStream");

type AppendEventArgs = TokenArgs & {
  readonly path: string;
  readonly dataJson: string;
};
const appendEventFn = makeFunctionReference<
  "mutation",
  AppendEventArgs,
  AppendResult
>("fluePersistence:appendEvent");

type AppendEventOnceArgs = TokenArgs & {
  readonly path: string;
  readonly key: string;
  readonly dataJson: string;
};
const appendEventOnce = makeFunctionReference<
  "mutation",
  AppendEventOnceArgs,
  AppendResult
>("fluePersistence:appendEventOnce");

type ReadEventsArgs = TokenArgs & {
  readonly path: string;
  readonly afterOffset: number;
  readonly limit: number;
};
interface ReadEventsResult {
  readonly events: EventEntryRow[];
  readonly nextOffset: number;
  readonly upToDate: boolean;
  readonly closed: boolean;
}
const readEventsFn = makeFunctionReference<
  "query",
  ReadEventsArgs,
  ReadEventsResult
>("fluePersistence:readEvents");

const closeEventStream = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly path: string },
  void
>("fluePersistence:closeEventStream");

interface EventStreamMetaRow { readonly nextOffset: number; readonly closed: boolean }
const getEventStreamMeta = makeFunctionReference<
  "query",
  TokenArgs & { readonly path: string },
  EventStreamMetaRow | null
>("fluePersistence:getEventStreamMeta");

// Run store
type CreateRunArgs = TokenArgs & {
  readonly runId: string;
  readonly workflowName: string;
  readonly startedAt: string;
  readonly inputJson: string;
  readonly traceCarrierJson?: string;
};
const createRun = makeFunctionReference<"mutation", CreateRunArgs, void>(
  "fluePersistence:createRun",
);

type EndRunArgs = TokenArgs & {
  readonly runId: string;
  readonly endedAt: string;
  readonly isError: boolean;
  readonly durationMs: number;
  readonly resultJson?: string;
  readonly errorJson?: string;
};
const endRun = makeFunctionReference<"mutation", EndRunArgs, void>(
  "fluePersistence:endRun",
);

const getRun = makeFunctionReference<
  "query",
  TokenArgs & { readonly runId: string },
  RunRow | null
>("fluePersistence:getRun");

const lookupRun = makeFunctionReference<
  "query",
  TokenArgs & { readonly runId: string },
  { readonly runId: string; readonly workflowName: string } | null
>("fluePersistence:lookupRun");

type ListRunsArgs = TokenArgs & {
  readonly status?: RunStatus;
  readonly workflowName?: string;
  readonly limit: number;
  readonly cursor?: { readonly startedAt: string; readonly runId: string };
};
interface ListRunsResult {
  readonly rows: RunPointerRow[];
  readonly hasMore: boolean;
}
const listRuns = makeFunctionReference<"query", ListRunsArgs, ListRunsResult>(
  "fluePersistence:listRuns",
);

// Attachments
type PutAttachmentArgs = TokenArgs & {
  readonly streamPath: string;
  readonly conversationId: string;
  readonly attachment: AttachmentRefWire;
  readonly bytes: ArrayBuffer;
};
const putAttachment = makeFunctionReference<
  "mutation",
  PutAttachmentArgs,
  "inserted" | "existing" | "conflict"
>("fluePersistence:putAttachment");

type GetAttachmentArgs = TokenArgs & {
  readonly streamPath: string;
  readonly conversationId: string;
  readonly attachmentId: string;
};
interface AttachmentWire {
  readonly attachment: AttachmentRefWire;
  readonly bytes: ArrayBuffer;
}
const getAttachment = makeFunctionReference<
  "query",
  GetAttachmentArgs,
  AttachmentWire | null
>("fluePersistence:getAttachment");

const deleteAttachmentsForInstance = makeFunctionReference<
  "mutation",
  TokenArgs & { readonly streamPath: string },
  void
>("fluePersistence:deleteAttachmentsForInstance");

// ---------------------------------------------------------------------------
// Marshalling helpers
// ---------------------------------------------------------------------------

const safeJsonParse = <T>(text: string | null | undefined): T | undefined => {
  if (text === null || text === undefined || text === "") {return undefined;}
  return JSON.parse(text) as T;
};

const jsonStringify = (value: unknown): string => JSON.stringify(value);

const parseAcceptedAt = (value: string, label: string): number => {
  const ms = Date.parse(value);
  if (!Number.isFinite(ms)) {
    throw new TypeError(
      `[flue] ${label} produced non-finite acceptedAt: ${value}`,
    );
  }
  return ms;
};

/** Parse a Flue offset string into a numeric `afterOffset` for backend reads. */
const afterOffset = (offset: string | undefined): number =>
  offset === undefined ? -1 : parseOffset(offset);

const wireRefToAttachmentRef = (wire: AttachmentRefWire): AttachmentRef => ({
  digest: wire.digest,
  id: wire.id,
  mimeType: wire.mimeType,
  size: wire.size,
  ...(wire.filename === undefined ? {} : { filename: wire.filename }),
});

const attachmentRefToWire = (ref: AttachmentRef): AttachmentRefWire => ({
  digest: ref.digest,
  id: ref.id,
  mimeType: ref.mimeType,
  size: ref.size,
  ...(ref.filename === undefined ? {} : { filename: ref.filename }),
});

/**
 * Rehydrate a {@link SubmissionRow} into the {@link AgentSubmission} contract.
 * `dispatch` inputs round-trip through `inputJson`; `direct` inputs are split
 * into `inputJson` (images stripped) + `chunksJson` and rehydrated with Flue's
 * `hydratePersistedDirectSubmission`.
 */
const hydrateSubmission = (row: SubmissionRow): AgentSubmission => {
  const baseSubmission: Omit<AgentSubmission, "input"> = {
    acceptedAt: row.acceptedAt,
    attemptCount: row.attemptCount,
    canonicalReadyAt: row.canonicalReadyAt,
    kind: row.kind,
    leaseExpiresAt: row.leaseExpiresAt,
    maxRetry: row.maxRetry,
    sequence: row.sequence,
    sessionKey: row.sessionKey,
    status: row.status,
    submissionId: row.submissionId,
    timeoutAt: row.timeoutAt,
    ...(row.attemptId === null ? {} : { attemptId: row.attemptId }),
    ...(row.inputAppliedAt === null
      ? {}
      : { inputAppliedAt: row.inputAppliedAt }),
    ...(row.recoveryRequestedAt === null
      ? {}
      : { recoveryRequestedAt: row.recoveryRequestedAt }),
    ...(row.abortRequestedAt === null
      ? {}
      : { abortRequestedAt: row.abortRequestedAt }),
    ...(row.startedAt === null ? {} : { startedAt: row.startedAt }),
    ...(row.error === null ? {} : { error: row.error }),
    ...(row.ownerId === null ? {} : { ownerId: row.ownerId }),
  };

  const traceCarrier = safeJsonParse<TraceCarrier>(row.traceCarrierJson);

  if (row.kind === "dispatch") {
    const input = safeJsonParse<DispatchAgentSubmissionInput>(row.inputJson);
    if (input === undefined) {
      throw new Error(
        `[flue] persisted dispatch submission ${row.submissionId} has empty inputJson`,
      );
    }
    const inputWithCarrier =
      traceCarrier === undefined ? input : { ...input, traceCarrier };
    return { ...baseSubmission, input: inputWithCarrier };
  }

  const stripped = safeJsonParse<DirectAgentSubmissionInput>(row.inputJson);
  if (stripped === undefined) {
    throw new Error(
      `[flue] persisted direct submission ${row.submissionId} has empty inputJson`,
    );
  }
  const chunks = safeJsonParse<PersistedChunkRow[]>(row.chunksJson) ?? [];
  const hydrated = hydratePersistedDirectSubmission(stripped, chunks);
  const input =
    traceCarrier === undefined ? hydrated : { ...hydrated, traceCarrier };
  return { ...baseSubmission, input };
};

const hydrateObligation = (
  row: SettlementObligationRow,
): SubmissionSettlementObligation => {
  const record = safeJsonParse<SubmissionSettledRecord>(row.recordJson);
  if (record === undefined) {
    throw new Error(
      `[flue] persisted settlement obligation ${row.submissionId} has empty recordJson`,
    );
  }
  return {
    attemptId: row.attemptId,
    record,
    recordId: row.recordId,
    sessionKey: row.sessionKey,
    submissionId: row.submissionId,
  };
};

const toRunRecord = (row: RunRow): RunRecord => ({
  runId: row.runId,
  startedAt: row.startedAt,
  status: row.status,
  workflowName: row.workflowName,
  ...(row.endedAt === null ? {} : { endedAt: row.endedAt }),
  ...(row.isError === null ? {} : { isError: row.isError }),
  ...(row.durationMs === null ? {} : { durationMs: row.durationMs }),
  ...(row.inputJson === null ? {} : { input: safeJsonParse(row.inputJson) }),
  ...(row.resultJson === null ? {} : { result: safeJsonParse(row.resultJson) }),
  ...(row.errorJson === null ? {} : { error: safeJsonParse(row.errorJson) }),
  ...(row.traceCarrierJson === null
    ? {}
    : { traceCarrier: safeJsonParse(row.traceCarrierJson) }),
});

const toRunPointer = (row: RunPointerRow): RunPointer => ({
  runId: row.runId,
  startedAt: row.startedAt,
  status: row.status,
  workflowName: row.workflowName,
  ...(row.endedAt === null ? {} : { endedAt: row.endedAt }),
  ...(row.durationMs === null ? {} : { durationMs: row.durationMs }),
  ...(row.isError === null ? {} : { isError: row.isError }),
});

// ---------------------------------------------------------------------------
// Client wrapper
//
// A typed facade over ConvexHttpClient. The generic methods constrain the
// function reference to the proper FunctionReference kind ("query" / "mutation")
// and infer args + return type, so call sites stay type-safe without casts.
// ---------------------------------------------------------------------------

class ConvexClient {
  private readonly client: ConvexHttpClient;

  constructor(url: string) {
    this.client = new ConvexHttpClient(url);
  }

  query<F extends FunctionReference<"query">>(
    ref: F,
    args: FunctionArgs<F>,
  ): Promise<FunctionReturnType<F>> {
    return this.client.query(ref, args);
  }

  mutation<F extends FunctionReference<"mutation">>(
    ref: F,
    args: FunctionArgs<F>,
  ): Promise<FunctionReturnType<F>> {
    return this.client.mutation(ref, args);
  }
}

// ---------------------------------------------------------------------------
// AgentSubmissionStore
// ---------------------------------------------------------------------------

class ConvexAgentSubmissionStore implements AgentSubmissionStore {
  constructor(private readonly convex: ConvexClient) {}

  async getSubmission(submissionId: string): Promise<AgentSubmission | null> {
    const row = await this.convex.query(getSubmission, {
      submissionId,
      token: TOKEN(),
    });
    return row === null ? null : hydrateSubmission(row);
  }

  hasUnsettledSubmissions(): Promise<boolean> {
    return this.convex.query(hasUnsettledSubmissions, { token: TOKEN() });
  }

  async listRunnableSubmissions(): Promise<AgentSubmission[]> {
    const rows = await this.convex.query(listRunnableSubmissions, {
      token: TOKEN(),
    });
    return rows.map(hydrateSubmission);
  }

  async listUnreadySubmissions(): Promise<AgentSubmission[]> {
    const rows = await this.convex.query(listUnreadySubmissions, {
      token: TOKEN(),
    });
    return rows.map(hydrateSubmission);
  }

  async listRunningSubmissions(): Promise<AgentSubmission[]> {
    const rows = await this.convex.query(listRunningSubmissions, {
      token: TOKEN(),
    });
    return rows.map(hydrateSubmission);
  }

  async listPendingSubmissionSettlements(): Promise<
    SubmissionSettlementObligation[]
  > {
    const rows = await this.convex.query(listPendingSubmissionSettlements, {
      token: TOKEN(),
    });
    return rows.map(hydrateObligation);
  }

  async replaceSubmissionAttempt(
    attempt: SubmissionAttemptRef,
    nextAttemptId: string,
    lease?: { ownerId: string; leaseExpiresAt: number },
  ): Promise<AgentSubmission | null> {
    const row = await this.convex.mutation(replaceSubmissionAttempt, {
      attemptId: attempt.attemptId,
      nextAttemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
      ...(lease === undefined ? {} : lease),
    });
    return row === null ? null : hydrateSubmission(row);
  }

  async admitDispatch(input: DispatchInput): Promise<AgentDispatchAdmission> {
    const sessionKey = createSessionStorageKey(input.id, "default", "default");
    const submissionInput: Omit<
      DispatchAgentSubmissionInput,
      "kind" | "submissionId"
    > = {
      acceptedAt: input.acceptedAt,
      agent: input.agent,
      dispatchId: input.dispatchId,
      id: input.id,
      input: input.input,
    };
    const res = await this.convex.mutation(admitSubmission, {
      input: {
        acceptedAt: parseAcceptedAt(input.acceptedAt, "admitDispatch"),
        inputJson: jsonStringify({
          kind: "dispatch",
          submissionId: input.dispatchId,
          ...submissionInput,
        }),
        kind: "dispatch",
        sessionKey,
        submissionId: input.dispatchId,
      },
      token: TOKEN(),
    });
    if (res.kind === "submission" && res.submission !== undefined) {
      return {
        kind: "submission",
        submission: hydrateSubmission(res.submission),
      };
    }
    if (res.kind === "retained_receipt" && res.receipt !== undefined) {
      return { kind: "retained_receipt", receipt: res.receipt };
    }
    return { kind: "conflict" };
  }

  async admitDirect(
    input: DirectAgentSubmissionInput,
  ): Promise<AgentSubmission> {
    const sessionKey = createSessionStorageKey(input.id, "default", "default");
    const extracted = prepareDirectSubmission(input);
    const res = await this.convex.mutation(admitSubmission, {
      input: {
        acceptedAt: parseAcceptedAt(input.acceptedAt, "admitDirect"),
        chunksJson: jsonStringify(extracted.chunks),
        inputJson: jsonStringify(extracted.value),
        kind: "direct",
        sessionKey,
        submissionId: input.submissionId,
        ...(input.traceCarrier === undefined
          ? {}
          : { traceCarrierJson: jsonStringify(input.traceCarrier) }),
      },
      token: TOKEN(),
    });
    if (res.kind === "submission" && res.submission !== undefined) {
      return hydrateSubmission(res.submission);
    }
    // Idempotent direct admission should always surface the existing
    // submission; if the backend signals a non-submission result we cannot
    // satisfy the `AgentSubmission` return type.
    throw new Error(
      `[flue] admitDirect for ${input.submissionId} did not return a submission`,
    );
  }

  async markSubmissionCanonicalReady(
    submissionId: string,
  ): Promise<AgentSubmission | null> {
    const row = await this.convex.mutation(markSubmissionCanonicalReady, {
      submissionId,
      token: TOKEN(),
    });
    return row === null ? null : hydrateSubmission(row);
  }

  async claimSubmission(
    claim: SubmissionClaimRef,
  ): Promise<AgentSubmission | null> {
    const row = await this.convex.mutation(claimSubmission, {
      attemptId: claim.attemptId,
      leaseExpiresAt: claim.leaseExpiresAt,
      ownerId: claim.ownerId,
      submissionId: claim.submissionId,
      token: TOKEN(),
    });
    return row === null ? null : hydrateSubmission(row);
  }

  markSubmissionInputApplied(
    attempt: SubmissionAttemptRef,
    durability?: SubmissionDurability,
  ): Promise<boolean> {
    return this.convex.mutation(markSubmissionInputApplied, {
      attemptId: attempt.attemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
      ...(durability === undefined ? {} : durability),
    });
  }

  requestSubmissionRecovery(
    attempt: SubmissionAttemptRef,
  ): Promise<boolean> {
    return this.convex.mutation(requestSubmissionRecovery, {
      attemptId: attempt.attemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  requestSessionAbort(sessionKey: string): Promise<string[]> {
    return this.convex.mutation(requestSessionAbort, {
      sessionKey,
      token: TOKEN(),
    });
  }

  requeueSubmissionBeforeInputApplied(
    attempt: SubmissionAttemptRef,
  ): Promise<boolean> {
    return this.convex.mutation(requeueSubmissionBeforeInputApplied, {
      attemptId: attempt.attemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  async reserveSubmissionSettlement(
    attempt: SubmissionAttemptRef,
    settlement: { recordId: string; record: SubmissionSettledRecord },
  ): Promise<SubmissionSettlementObligation | null> {
    const row = await this.convex.mutation(reserveSubmissionSettlement, {
      attemptId: attempt.attemptId,
      recordId: settlement.recordId,
      recordJson: jsonStringify(settlement.record),
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
    return row === null ? null : hydrateObligation(row);
  }

  finalizeSubmissionSettlement(
    attempt: SubmissionAttemptRef,
    recordId: string,
  ): Promise<boolean> {
    return this.convex.mutation(finalizeSubmissionSettlement, {
      attemptId: attempt.attemptId,
      recordId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  completeSubmission(attempt: SubmissionAttemptRef): Promise<boolean> {
    return this.convex.mutation(settleSubmission, {
      attemptId: attempt.attemptId,
      outcome: "completed",
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  failSubmission(
    attempt: SubmissionAttemptRef,
    error: unknown,
  ): Promise<boolean> {
    return this.convex.mutation(settleSubmission, {
      attemptId: attempt.attemptId,
      errorJson: jsonStringify(error),
      outcome: "failed",
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  async insertAttemptMarker(attempt: SubmissionAttemptRef): Promise<void> {
    await this.convex.mutation(insertAttemptMarker, {
      attemptId: attempt.attemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  async deleteAttemptMarker(attempt: SubmissionAttemptRef): Promise<void> {
    await this.convex.mutation(deleteAttemptMarker, {
      attemptId: attempt.attemptId,
      submissionId: attempt.submissionId,
      token: TOKEN(),
    });
  }

  async listAttemptMarkers(): Promise<AgentAttemptMarker[]> {
    const rows = await this.convex.query(listAttemptMarkers, {
      token: TOKEN(),
    });
    return rows.map((row) => ({
      attemptId: row.attemptId,
      createdAt: row.createdAt,
      submissionId: row.submissionId,
    }));
  }

  async renewLeases(ownerId: string, submissionIds: string[]): Promise<void> {
    await this.convex.mutation(renewLeases, {
      ownerId,
      submissionIds,
      token: TOKEN(),
    });
  }

  async listExpiredSubmissions(): Promise<AgentSubmission[]> {
    const rows = await this.convex.query(listExpiredSubmissions, {
      token: TOKEN(),
    });
    return rows.map(hydrateSubmission);
  }
}

// ---------------------------------------------------------------------------
// ConversationStreamStore
// ---------------------------------------------------------------------------

class ConvexConversationStreamStore implements ConversationStreamStore {
  private readonly listeners = new StreamListenerRegistry();

  constructor(private readonly convex: ConvexClient) {}

  async createStream(
    path: string,
    identity: ConversationStreamIdentity,
  ): Promise<void> {
    await this.convex.mutation(createConversationStream, {
      identity,
      path,
      token: TOKEN(),
    });
  }

  acquireProducer(
    path: string,
    producerId: string,
  ): Promise<ConversationProducerClaim> {
    return this.convex.mutation(acquireConversationProducer, {
      path,
      producerId,
      token: TOKEN(),
    });
  }

  async append(input: {
    path: string;
    producerId: string;
    producerEpoch: number;
    incarnation: string;
    producerSequence: number;
    submission?: { submissionId: string; attemptId: string };
    records: readonly ConversationRecord[];
  }): Promise<{ offset: string }> {
    const result = await this.convex.mutation(appendConversationBatch, {
      incarnation: input.incarnation,
      path: input.path,
      producerEpoch: input.producerEpoch,
      producerId: input.producerId,
      producerSequence: input.producerSequence,
      recordsJson: jsonStringify(input.records),
      token: TOKEN(),
      ...(input.submission === undefined ? {} : { submission: input.submission }),
    });
    if (result.appended) {
      this.listeners.notify(input.path);
    }
    return { offset: formatOffset(result.offset) };
  }

  async read(
    path: string,
    options?: { offset?: string; limit?: number },
  ): Promise<ConversationStreamReadResult> {
    const limit = clampLimit(
      options?.limit,
      DEFAULT_READ_LIMIT,
      MAX_READ_LIMIT,
    );
    const result = await this.convex.query(readConversationBatches, {
      afterOffset: afterOffset(options?.offset),
      limit,
      path,
      token: TOKEN(),
    });
    const batches: ConversationStreamBatch[] = result.batches.map((row) => ({
      offset: formatOffset(row.offset),
      records: safeJsonParse<ConversationRecord[]>(row.recordsJson) ?? [],
    }));
    return {
      batches,
      nextOffset: formatOffset(result.nextOffset),
      upToDate: result.upToDate,
    };
  }

  async getMeta(path: string): Promise<ConversationStreamMeta | null> {
    const row = await this.convex.query(getConversationStreamMeta, {
      path,
      token: TOKEN(),
    });
    if (row === null) {return null;}
    return {
      identity: row.identity,
      incarnation: row.incarnation,
      nextOffset: formatOffset(row.nextOffset),
      nextProducerSequence: row.nextProducerSequence,
      producerEpoch: row.producerEpoch,
      producerId: row.producerId,
    };
  }

  async delete(path: string): Promise<void> {
    await this.convex.mutation(deleteConversationStream, {
      path,
      token: TOKEN(),
    });
  }

  subscribe(path: string, listener: () => void): () => void {
    return this.listeners.subscribe(path, listener);
  }
}

// ---------------------------------------------------------------------------
// EventStreamStore
// ---------------------------------------------------------------------------

class ConvexEventStreamStore implements EventStreamStore {
  private readonly listeners = new StreamListenerRegistry();

  constructor(private readonly convex: ConvexClient) {}

  async createStream(path: string): Promise<void> {
    await this.convex.mutation(createEventStream, {
      path,
      token: TOKEN(),
    });
  }

  async appendEvent(path: string, event: unknown): Promise<string> {
    const result = await this.convex.mutation(appendEventFn, {
      dataJson: jsonStringify(event),
      path,
      token: TOKEN(),
    });
    if (result.appended) {
      this.listeners.notify(path);
    }
    return formatOffset(result.offset);
  }

  async appendEventOnce(
    path: string,
    key: string,
    event: unknown,
  ): Promise<string> {
    const result = await this.convex.mutation(appendEventOnce, {
      dataJson: jsonStringify(event),
      key,
      path,
      token: TOKEN(),
    });
    if (result.appended) {
      this.listeners.notify(path);
    }
    return formatOffset(result.offset);
  }

  async readEvents(
    path: string,
    opts?: { offset?: string; limit?: number },
  ): Promise<EventStreamReadResult> {
    const limit = clampLimit(opts?.limit, DEFAULT_READ_LIMIT, MAX_READ_LIMIT);
    const result = await this.convex.query(readEventsFn, {
      afterOffset: afterOffset(opts?.offset),
      limit,
      path,
      token: TOKEN(),
    });
    return {
      closed: result.closed,
      events: result.events.map((row) => ({
        data: safeJsonParse(row.dataJson),
        offset: formatOffset(row.offset),
      })),
      nextOffset: formatOffset(result.nextOffset),
      upToDate: result.upToDate,
    };
  }

  async closeStream(path: string): Promise<void> {
    await this.convex.mutation(closeEventStream, {
      path,
      token: TOKEN(),
    });
  }

  async getStreamMeta(path: string): Promise<EventStreamMeta | null> {
    const row = await this.convex.query(getEventStreamMeta, {
      path,
      token: TOKEN(),
    });
    if (row === null) {return null;}
    return {
      closed: row.closed,
      nextOffset: formatOffset(row.nextOffset),
    };
  }

  subscribe(path: string, listener: () => void): () => void {
    return this.listeners.subscribe(path, listener);
  }
}

// ---------------------------------------------------------------------------
// RunStore
// ---------------------------------------------------------------------------

class ConvexRunStore implements RunStore {
  constructor(private readonly convex: ConvexClient) {}

  async createRun(input: CreateRunInput): Promise<void> {
    await this.convex.mutation(createRun, {
      inputJson: jsonStringify(input.input),
      runId: input.runId,
      startedAt: input.startedAt,
      token: TOKEN(),
      workflowName: input.workflowName,
      ...(input.traceCarrier === undefined
        ? {}
        : { traceCarrierJson: jsonStringify(input.traceCarrier) }),
    });
  }

  async endRun(input: EndRunInput): Promise<void> {
    await this.convex.mutation(endRun, {
      durationMs: input.durationMs,
      endedAt: input.endedAt,
      isError: input.isError,
      runId: input.runId,
      token: TOKEN(),
      ...(input.result === undefined
        ? {}
        : { resultJson: jsonStringify(input.result) }),
      ...(input.error === undefined
        ? {}
        : { errorJson: jsonStringify(input.error) }),
    });
  }

  async getRun(runId: string): Promise<RunRecord | null> {
    const row = await this.convex.query(getRun, { runId, token: TOKEN() });
    return row === null ? null : toRunRecord(row);
  }

  lookupRun(
    runId: string,
  ): Promise<{ runId: string; workflowName: string } | null> {
    return this.convex.query(lookupRun, { runId, token: TOKEN() });
  }

  async listRuns(opts?: {
    status?: RunStatus;
    workflowName?: string;
    limit?: number;
    cursor?: string;
  }): Promise<{ runs: RunPointer[]; nextCursor?: string }> {
    const limit = clampLimit(opts?.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
    const decoded = decodeRunCursor(opts?.cursor);
    const result = await this.convex.query(listRuns, {
      token: TOKEN(),
      ...(opts?.status === undefined ? {} : { status: opts.status }),
      ...(opts?.workflowName === undefined
        ? {}
        : { workflowName: opts.workflowName }),
      limit,
      ...(decoded === undefined ? {} : { cursor: decoded }),
    });
    const runs = result.rows.map(toRunPointer);
    const last = runs.at(-1);
    const nextCursor =
      result.hasMore && last !== undefined
        ? encodeRunCursor({ runId: last.runId, startedAt: last.startedAt })
        : undefined;
    return { runs, ...(nextCursor === undefined ? {} : { nextCursor }) };
  }
}

// ---------------------------------------------------------------------------
// AttachmentStore
// ---------------------------------------------------------------------------

class ConvexAttachmentStore implements AttachmentStore {
  constructor(private readonly convex: ConvexClient) {}

  async put(input: PutAttachmentInput): Promise<void> {
    // Defensive copy: callers hand us a Uint8Array view that may be backed by
    // a larger buffer; we encode an immutable ArrayBuffer slice so the wire
    // payload cannot be mutated after the call returns.
    const bytes = copyAttachmentBytes(input.bytes);
    const status = await this.convex.mutation(putAttachment, {
      attachment: attachmentRefToWire(input.attachment),
      bytes: bytes.buffer.slice(
        bytes.byteOffset,
        bytes.byteOffset + bytes.byteLength,
      ) as ArrayBuffer,
      conversationId: input.conversationId,
      streamPath: input.streamPath,
      token: TOKEN(),
    });
    if (status === "conflict") {
      throw new AttachmentConflictError({
        attachmentId: input.attachment.id,
        path: input.streamPath,
      });
    }
  }

  async get(input: GetAttachmentInput): Promise<StoredAttachment | null> {
    const row = await this.convex.query(getAttachment, {
      attachmentId: input.attachmentId,
      conversationId: input.conversationId,
      streamPath: input.streamPath,
      token: TOKEN(),
    });
    if (row === null) {return null;}
    const attachment = wireRefToAttachmentRef(row.attachment);
    const bytes = new Uint8Array(row.bytes);
    // Verify integrity before handing bytes back to the runtime; throws
    // AttachmentIntegrityError on size/digest mismatch.
    await verifyAttachmentBytes(attachment, bytes);
    return {
      attachment,
      bytes: copyAttachmentBytes(bytes),
    };
  }

  async deleteForInstance(streamPath: string): Promise<void> {
    await this.convex.mutation(deleteAttachmentsForInstance, {
      streamPath,
      token: TOKEN(),
    });
  }
}

// ---------------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------------

class ConvexPersistenceAdapterImpl implements PersistenceAdapter {
  private readonly convex: ConvexClient;
  private migrated = false;

  constructor() {
    this.convex = new ConvexClient(env.CONVEX_URL);
  }

  async migrate(): Promise<void> {
    const version = await this.convex.query(checkSchemaVersion, {
      token: TOKEN(),
    });
    assertSupportedFlueSchemaVersion(version);
    this.migrated = true;
  }

  connect(): PersistenceStores {
    if (!this.migrated) {
      throw new Error(
        "[flue] ConvexPersistenceAdapter.connect() called before migrate() completed",
      );
    }
    const submissions = new ConvexAgentSubmissionStore(this.convex);
    const executionStore: AgentExecutionStore = { submissions };
    return {
      attachmentStore: new ConvexAttachmentStore(this.convex),
      conversationStreamStore: new ConvexConversationStreamStore(this.convex),
      eventStreamStore: new ConvexEventStreamStore(this.convex),
      executionStore,
      runStore: new ConvexRunStore(this.convex),
    };
  }

  close(): void {
    // ConvexHttpClient has no explicit teardown; the connection is HTTP-based
    // and resources are released on process exit.
  }
}

const adapter: PersistenceAdapter = new ConvexPersistenceAdapterImpl();

export default adapter;
